1#![forbid(unsafe_code)]
39#![no_std]
40
41extern crate alloc;
42
43use alloc::fmt;
44use alloc::string::String;
45use alloc::vec::Vec;
46use core::marker::PhantomData;
47use core::str::FromStr;
48
49use serde::{Deserialize, Serialize};
50
51#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
69#[serde(transparent)]
70pub struct TraceId(
71 #[serde(with = "hex_u128")]
73 u128,
74);
75
76impl TraceId {
77 pub const fn from_raw(raw: u128) -> Self {
79 Self(raw)
80 }
81
82 pub const fn from_parts(ts_ms: u64, random: u128) -> Self {
87 let ts_bits = (ts_ms as u128) << 80;
88 let rand_bits = random & 0xFFFF_FFFF_FFFF_FFFF_FFFF; Self(ts_bits | rand_bits)
90 }
91
92 pub const fn timestamp_ms(self) -> u64 {
94 (self.0 >> 80) as u64
95 }
96
97 pub const fn as_u128(self) -> u128 {
99 self.0
100 }
101
102 pub const fn to_bytes(self) -> [u8; 16] {
104 self.0.to_be_bytes()
105 }
106
107 pub const fn from_bytes(bytes: [u8; 16]) -> Self {
109 Self(u128::from_be_bytes(bytes))
110 }
111}
112
113impl fmt::Debug for TraceId {
114 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115 write!(f, "TraceId({:032x})", self.0)
116 }
117}
118
119impl fmt::Display for TraceId {
120 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121 write!(f, "{:032x}", self.0)
122 }
123}
124
125impl FromStr for TraceId {
126 type Err = ParseIdError;
127
128 fn from_str(s: &str) -> Result<Self, Self::Err> {
129 let val = u128::from_str_radix(s, 16).map_err(|_| ParseIdError {
130 kind: "TraceId",
131 input_len: s.len(),
132 })?;
133 Ok(Self(val))
134 }
135}
136
137#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
145#[serde(transparent)]
146pub struct DecisionId(#[serde(with = "hex_u128")] u128);
147
148impl DecisionId {
149 pub const fn from_raw(raw: u128) -> Self {
151 Self(raw)
152 }
153
154 pub const fn from_parts(ts_ms: u64, random: u128) -> Self {
156 let ts_bits = (ts_ms as u128) << 80;
157 let rand_bits = random & 0xFFFF_FFFF_FFFF_FFFF_FFFF;
158 Self(ts_bits | rand_bits)
159 }
160
161 pub const fn timestamp_ms(self) -> u64 {
163 (self.0 >> 80) as u64
164 }
165
166 pub const fn as_u128(self) -> u128 {
168 self.0
169 }
170
171 pub const fn to_bytes(self) -> [u8; 16] {
173 self.0.to_be_bytes()
174 }
175
176 pub const fn from_bytes(bytes: [u8; 16]) -> Self {
178 Self(u128::from_be_bytes(bytes))
179 }
180}
181
182impl fmt::Debug for DecisionId {
183 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184 write!(f, "DecisionId({:032x})", self.0)
185 }
186}
187
188impl fmt::Display for DecisionId {
189 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190 write!(f, "{:032x}", self.0)
191 }
192}
193
194impl FromStr for DecisionId {
195 type Err = ParseIdError;
196
197 fn from_str(s: &str) -> Result<Self, Self::Err> {
198 let val = u128::from_str_radix(s, 16).map_err(|_| ParseIdError {
199 kind: "DecisionId",
200 input_len: s.len(),
201 })?;
202 Ok(Self(val))
203 }
204}
205
206#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
222pub struct PolicyId {
223 #[serde(rename = "n")]
225 name: String,
226 #[serde(rename = "v")]
228 version: u32,
229}
230
231impl PolicyId {
232 pub fn new(name: impl Into<String>, version: u32) -> Self {
234 Self {
235 name: name.into(),
236 version,
237 }
238 }
239
240 pub fn name(&self) -> &str {
242 &self.name
243 }
244
245 pub const fn version(&self) -> u32 {
247 self.version
248 }
249}
250
251impl fmt::Display for PolicyId {
252 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
253 write!(f, "{}@v{}", self.name, self.version)
254 }
255}
256
257#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
276pub struct SchemaVersion {
277 pub major: u32,
279 pub minor: u32,
281 pub patch: u32,
283}
284
285impl SchemaVersion {
286 pub const fn new(major: u32, minor: u32, patch: u32) -> Self {
288 Self {
289 major,
290 minor,
291 patch,
292 }
293 }
294
295 pub const fn is_compatible(&self, other: &Self) -> bool {
297 self.major == other.major
298 }
299}
300
301impl fmt::Display for SchemaVersion {
302 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
303 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
304 }
305}
306
307impl FromStr for SchemaVersion {
308 type Err = ParseVersionError;
309
310 fn from_str(s: &str) -> Result<Self, Self::Err> {
311 let parts: alloc::vec::Vec<&str> = s.split('.').collect();
312 if parts.len() != 3 {
313 return Err(ParseVersionError);
314 }
315 let major = parts[0].parse().map_err(|_| ParseVersionError)?;
316 let minor = parts[1].parse().map_err(|_| ParseVersionError)?;
317 let patch = parts[2].parse().map_err(|_| ParseVersionError)?;
318 Ok(Self {
319 major,
320 minor,
321 patch,
322 })
323 }
324}
325
326#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
344pub struct Budget {
345 remaining_ms: u64,
346}
347
348impl Budget {
349 pub const fn new(ms: u64) -> Self {
351 Self { remaining_ms: ms }
352 }
353
354 pub const fn remaining_ms(self) -> u64 {
356 self.remaining_ms
357 }
358
359 pub const fn consume(self, ms: u64) -> Option<Self> {
363 if self.remaining_ms >= ms {
364 Some(Self {
365 remaining_ms: self.remaining_ms - ms,
366 })
367 } else {
368 None
369 }
370 }
371
372 pub const fn is_exhausted(self) -> bool {
374 self.remaining_ms == 0
375 }
376
377 #[must_use]
379 pub const fn min(self, other: Self) -> Self {
380 if self.remaining_ms <= other.remaining_ms {
381 self
382 } else {
383 other
384 }
385 }
386
387 pub const UNLIMITED: Self = Self {
389 remaining_ms: u64::MAX,
390 };
391}
392
393pub trait CapabilitySet: Clone + fmt::Debug + Send + Sync {
406 fn capability_names(&self) -> Vec<&str>;
408
409 fn count(&self) -> usize;
411
412 fn is_empty(&self) -> bool {
414 self.count() == 0
415 }
416}
417
418#[derive(Clone, Debug, Default, PartialEq, Eq)]
420pub struct NoCaps;
421
422impl CapabilitySet for NoCaps {
423 fn capability_names(&self) -> Vec<&str> {
424 Vec::new()
425 }
426
427 fn count(&self) -> usize {
428 0
429 }
430}
431
432pub struct Cx<'a, C: CapabilitySet = NoCaps> {
455 trace_id: TraceId,
456 budget: Budget,
457 capabilities: C,
458 depth: u32,
459 _scope: PhantomData<&'a ()>,
460}
461
462impl<C: CapabilitySet> Cx<'_, C> {
463 pub fn new(trace_id: TraceId, budget: Budget, capabilities: C) -> Self {
465 Self {
466 trace_id,
467 budget,
468 capabilities,
469 depth: 0,
470 _scope: PhantomData,
471 }
472 }
473
474 pub fn child(&self, capabilities: C, budget: Budget) -> Cx<'_, C> {
479 Cx {
480 trace_id: self.trace_id,
481 budget: self.budget.min(budget),
482 capabilities,
483 depth: self.depth + 1,
484 _scope: PhantomData,
485 }
486 }
487
488 pub const fn trace_id(&self) -> TraceId {
490 self.trace_id
491 }
492
493 pub const fn budget(&self) -> Budget {
495 self.budget
496 }
497
498 pub fn capabilities(&self) -> &C {
500 &self.capabilities
501 }
502
503 pub const fn depth(&self) -> u32 {
505 self.depth
506 }
507
508 pub fn consume_budget(&mut self, ms: u64) -> bool {
512 match self.budget.consume(ms) {
513 Some(new_budget) => {
514 self.budget = new_budget;
515 true
516 }
517 None => false,
518 }
519 }
520}
521
522impl<C: CapabilitySet> fmt::Debug for Cx<'_, C> {
523 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
524 f.debug_struct("Cx")
525 .field("trace_id", &self.trace_id)
526 .field("budget_ms", &self.budget.remaining_ms())
527 .field("capabilities", &self.capabilities)
528 .field("depth", &self.depth)
529 .finish()
530 }
531}
532
533#[derive(Clone, Debug, PartialEq, Eq)]
539pub struct ParseIdError {
540 pub kind: &'static str,
542 pub input_len: usize,
544}
545
546impl fmt::Display for ParseIdError {
547 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
548 write!(
549 f,
550 "invalid {} hex string (length {})",
551 self.kind, self.input_len
552 )
553 }
554}
555
556#[derive(Clone, Debug, PartialEq, Eq)]
558pub struct ParseVersionError;
559
560impl fmt::Display for ParseVersionError {
561 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
562 write!(f, "invalid schema version (expected major.minor.patch)")
563 }
564}
565
566mod hex_u128 {
571 use alloc::format;
572 use alloc::string::String;
573
574 use serde::{self, Deserialize, Deserializer, Serializer};
575
576 pub fn serialize<S>(value: &u128, serializer: S) -> Result<S::Ok, S::Error>
577 where
578 S: Serializer,
579 {
580 serializer.serialize_str(&format!("{value:032x}"))
581 }
582
583 pub fn deserialize<'de, D>(deserializer: D) -> Result<u128, D::Error>
584 where
585 D: Deserializer<'de>,
586 {
587 let s = String::deserialize(deserializer)?;
588 u128::from_str_radix(&s, 16)
589 .map_err(|_| serde::de::Error::custom(format!("invalid hex u128: {s}")))
590 }
591}
592
593#[cfg(test)]
598mod tests {
599 extern crate std;
600
601 use super::*;
602 use core::hash::{Hash, Hasher};
603 use std::collections::hash_map::DefaultHasher;
604 use std::string::ToString;
605
606 fn hash_of<T: Hash>(val: &T) -> u64 {
607 let mut h = DefaultHasher::new();
608 val.hash(&mut h);
609 h.finish()
610 }
611
612 #[test]
617 fn trace_id_from_parts_roundtrip() {
618 let ts = 1_700_000_000_000_u64;
619 let random = 0x00AB_CDEF_0123_4567_89AB_u128;
620 let id = TraceId::from_parts(ts, random);
621 assert_eq!(id.timestamp_ms(), ts);
622 assert_eq!(id.as_u128() & 0xFFFF_FFFF_FFFF_FFFF_FFFF, random);
623 }
624
625 #[test]
626 fn trace_id_display_parse_roundtrip() {
627 let id = TraceId::from_raw(0x0123_4567_89AB_CDEF_0123_4567_89AB_CDEF);
628 let hex = id.to_string();
629 assert_eq!(hex, "0123456789abcdef0123456789abcdef");
630 let parsed: TraceId = hex.parse().unwrap();
631 assert_eq!(id, parsed);
632 }
633
634 #[test]
635 fn trace_id_bytes_roundtrip() {
636 let id = TraceId::from_raw(42);
637 let bytes = id.to_bytes();
638 let recovered = TraceId::from_bytes(bytes);
639 assert_eq!(id, recovered);
640 }
641
642 #[test]
643 fn trace_id_ordering() {
644 let earlier = TraceId::from_parts(1000, 0);
645 let later = TraceId::from_parts(2000, 0);
646 assert!(earlier < later);
647 }
648
649 #[test]
650 fn trace_id_uuidv7_monotonic_ordering_10k() {
651 let ids: std::vec::Vec<TraceId> = (0..10_000)
653 .map(|i| TraceId::from_parts(1_700_000_000_000 + i, 0))
654 .collect();
655 for window in ids.windows(2) {
656 assert!(
657 window[0] < window[1],
658 "TraceId ordering violated: {:?} >= {:?}",
659 window[0],
660 window[1]
661 );
662 }
663 }
664
665 #[test]
666 fn trace_id_display_parse_roundtrip_many() {
667 for i in 0..10_000_u128 {
669 let raw = i.wrapping_mul(0x0123_4567_89AB_CDEF) ^ (i << 64);
670 let id = TraceId::from_raw(raw);
671 let hex = id.to_string();
672 let parsed: TraceId = hex.parse().unwrap();
673 assert_eq!(id, parsed, "roundtrip failed for raw={raw:#034x}");
674 }
675 }
676
677 #[test]
678 fn trace_id_serde_json() {
679 let id = TraceId::from_raw(0xFF);
680 let json = serde_json::to_string(&id).unwrap();
681 assert_eq!(json, "\"000000000000000000000000000000ff\"");
682 let parsed: TraceId = serde_json::from_str(&json).unwrap();
683 assert_eq!(id, parsed);
684 }
685
686 #[test]
687 fn trace_id_serde_roundtrip_many() {
688 for i in 0..1_000_u128 {
689 let id = TraceId::from_raw(i.wrapping_mul(0xDEAD_BEEF_CAFE_1234));
690 let json = serde_json::to_string(&id).unwrap();
691 let parsed: TraceId = serde_json::from_str(&json).unwrap();
692 assert_eq!(id, parsed);
693 }
694 }
695
696 #[test]
697 fn trace_id_debug_format() {
698 let id = TraceId::from_raw(0xAB);
699 let dbg = std::format!("{id:?}");
700 assert!(dbg.starts_with("TraceId("));
701 assert!(dbg.contains("ab"));
702 }
703
704 #[test]
705 fn trace_id_copy_semantics() {
706 let id = TraceId::from_raw(42);
707 let copy = id;
708 assert_eq!(id, copy); }
710
711 #[test]
712 fn trace_id_hash_consistency() {
713 let a = TraceId::from_raw(0xDEAD);
714 let b = TraceId::from_raw(0xDEAD);
715 assert_eq!(a, b);
716 assert_eq!(hash_of(&a), hash_of(&b));
717 }
718
719 #[test]
720 fn trace_id_zero_and_max() {
721 let zero = TraceId::from_raw(0);
722 assert_eq!(zero.timestamp_ms(), 0);
723 assert_eq!(zero.to_string(), "00000000000000000000000000000000");
724 let roundtrip: TraceId = zero.to_string().parse().unwrap();
725 assert_eq!(zero, roundtrip);
726
727 let max = TraceId::from_raw(u128::MAX);
728 assert_eq!(max.to_string(), "ffffffffffffffffffffffffffffffff");
729 let roundtrip: TraceId = max.to_string().parse().unwrap();
730 assert_eq!(max, roundtrip);
731 }
732
733 #[test]
738 fn decision_id_from_parts_roundtrip() {
739 let ts = 1_700_000_000_000_u64;
740 let random = 0x0012_3456_789A_BCDE_F012_u128;
741 let id = DecisionId::from_parts(ts, random);
742 assert_eq!(id.timestamp_ms(), ts);
743 assert_eq!(id.as_u128() & 0xFFFF_FFFF_FFFF_FFFF_FFFF, random);
744 }
745
746 #[test]
747 fn decision_id_display_parse_roundtrip() {
748 let id = DecisionId::from_raw(0xDEAD_BEEF);
749 let hex = id.to_string();
750 let parsed: DecisionId = hex.parse().unwrap();
751 assert_eq!(id, parsed);
752 }
753
754 #[test]
755 fn decision_id_display_parse_roundtrip_many() {
756 for i in 0..10_000_u128 {
757 let raw = i.wrapping_mul(0xABCD_EF01_2345_6789) ^ (i << 64);
758 let id = DecisionId::from_raw(raw);
759 let hex = id.to_string();
760 let parsed: DecisionId = hex.parse().unwrap();
761 assert_eq!(id, parsed, "roundtrip failed for raw={raw:#034x}");
762 }
763 }
764
765 #[test]
766 fn decision_id_ordering() {
767 let earlier = DecisionId::from_parts(1000, 0);
768 let later = DecisionId::from_parts(2000, 0);
769 assert!(earlier < later);
770 }
771
772 #[test]
773 fn decision_id_monotonic_ordering_10k() {
774 let ids: std::vec::Vec<DecisionId> = (0..10_000)
775 .map(|i| DecisionId::from_parts(1_700_000_000_000 + i, 0))
776 .collect();
777 for window in ids.windows(2) {
778 assert!(window[0] < window[1]);
779 }
780 }
781
782 #[test]
783 fn decision_id_serde_json() {
784 let id = DecisionId::from_raw(1);
785 let json = serde_json::to_string(&id).unwrap();
786 let parsed: DecisionId = serde_json::from_str(&json).unwrap();
787 assert_eq!(id, parsed);
788 }
789
790 #[test]
791 fn decision_id_debug_format() {
792 let id = DecisionId::from_raw(0xCD);
793 let dbg = std::format!("{id:?}");
794 assert!(dbg.starts_with("DecisionId("));
795 assert!(dbg.contains("cd"));
796 }
797
798 #[test]
799 fn decision_id_copy_semantics() {
800 let id = DecisionId::from_raw(99);
801 let copy = id;
802 assert_eq!(id, copy);
803 }
804
805 #[test]
806 fn decision_id_hash_consistency() {
807 let a = DecisionId::from_raw(0xBEEF);
808 let b = DecisionId::from_raw(0xBEEF);
809 assert_eq!(a, b);
810 assert_eq!(hash_of(&a), hash_of(&b));
811 }
812
813 #[test]
814 fn decision_id_bytes_roundtrip() {
815 let id = DecisionId::from_raw(0x1234_5678_9ABC_DEF0);
816 let bytes = id.to_bytes();
817 let recovered = DecisionId::from_bytes(bytes);
818 assert_eq!(id, recovered);
819 }
820
821 #[test]
826 fn policy_id_display() {
827 let policy = PolicyId::new("scheduler.preempt", 3);
828 assert_eq!(policy.to_string(), "scheduler.preempt@v3");
829 assert_eq!(policy.name(), "scheduler.preempt");
830 assert_eq!(policy.version(), 3);
831 }
832
833 #[test]
834 fn policy_id_serde_json() {
835 let policy = PolicyId::new("cancel.budget", 1);
836 let json = serde_json::to_string(&policy).unwrap();
837 assert!(json.contains("\"n\":"));
838 assert!(json.contains("\"v\":"));
839 let parsed: PolicyId = serde_json::from_str(&json).unwrap();
840 assert_eq!(policy, parsed);
841 }
842
843 #[test]
844 fn policy_id_ordering() {
845 let a = PolicyId::new("a.policy", 1);
846 let b = PolicyId::new("b.policy", 1);
847 assert!(a < b, "PolicyId should order lexicographically by name");
848 let v1 = PolicyId::new("same", 1);
849 let v2 = PolicyId::new("same", 2);
850 assert!(v1 < v2, "same name, should order by version");
851 }
852
853 #[test]
854 fn policy_id_hash_consistency() {
855 let a = PolicyId::new("test.policy", 5);
856 let b = PolicyId::new("test.policy", 5);
857 assert_eq!(a, b);
858 assert_eq!(hash_of(&a), hash_of(&b));
859 }
860
861 #[test]
866 fn schema_version_compatible() {
867 let v1_2_3 = SchemaVersion::new(1, 2, 3);
868 let v1_5_0 = SchemaVersion::new(1, 5, 0);
869 let v2_0_0 = SchemaVersion::new(2, 0, 0);
870 assert!(v1_2_3.is_compatible(&v1_5_0));
871 assert!(!v1_2_3.is_compatible(&v2_0_0));
872 }
873
874 #[test]
875 fn schema_version_0x_edge_cases() {
876 let v0_1 = SchemaVersion::new(0, 1, 0);
879 let v0_2 = SchemaVersion::new(0, 2, 0);
880 assert!(
881 v0_1.is_compatible(&v0_2),
882 "0.x versions should be compatible (same major=0)"
883 );
884
885 let v1_0 = SchemaVersion::new(1, 0, 0);
887 assert!(!v0_1.is_compatible(&v1_0));
888 }
889
890 #[test]
891 fn schema_version_display_parse_roundtrip() {
892 let v = SchemaVersion::new(1, 2, 3);
893 assert_eq!(v.to_string(), "1.2.3");
894 let parsed: SchemaVersion = "1.2.3".parse().unwrap();
895 assert_eq!(v, parsed);
896 }
897
898 #[test]
899 fn schema_version_ordering_comprehensive() {
900 let versions = [
901 SchemaVersion::new(1, 0, 0),
902 SchemaVersion::new(1, 0, 1),
903 SchemaVersion::new(1, 1, 0),
904 SchemaVersion::new(2, 0, 0),
905 SchemaVersion::new(2, 1, 0),
906 SchemaVersion::new(10, 0, 0),
907 ];
908 for window in versions.windows(2) {
909 assert!(
910 window[0] < window[1],
911 "{} should be < {}",
912 window[0],
913 window[1]
914 );
915 }
916 }
917
918 #[test]
919 fn schema_version_ordering() {
920 let v1 = SchemaVersion::new(1, 0, 0);
921 let v2 = SchemaVersion::new(2, 0, 0);
922 assert!(v1 < v2);
923 }
924
925 #[test]
926 fn schema_version_serde_json() {
927 let v = SchemaVersion::new(3, 1, 4);
928 let json = serde_json::to_string(&v).unwrap();
929 let parsed: SchemaVersion = serde_json::from_str(&json).unwrap();
930 assert_eq!(v, parsed);
931 }
932
933 #[test]
934 fn schema_version_copy_semantics() {
935 let v = SchemaVersion::new(1, 0, 0);
936 let copy = v;
937 assert_eq!(v, copy);
938 }
939
940 #[test]
941 fn schema_version_hash_consistency() {
942 let a = SchemaVersion::new(1, 2, 3);
943 let b = SchemaVersion::new(1, 2, 3);
944 assert_eq!(a, b);
945 assert_eq!(hash_of(&a), hash_of(&b));
946 }
947
948 #[test]
949 fn schema_version_self_compatible() {
950 let v = SchemaVersion::new(5, 3, 1);
951 assert!(
952 v.is_compatible(&v),
953 "version must be compatible with itself"
954 );
955 }
956
957 #[test]
962 fn parse_id_error_display() {
963 let err = ParseIdError {
964 kind: "TraceId",
965 input_len: 5,
966 };
967 let msg = err.to_string();
968 assert!(msg.contains("TraceId"));
969 assert!(msg.contains('5'));
970 }
971
972 #[test]
973 fn parse_version_error_display() {
974 let err = ParseVersionError;
975 let msg = err.to_string();
976 assert!(msg.contains("major.minor.patch"));
977 }
978
979 #[test]
980 fn invalid_hex_parse_fails() {
981 assert!("not-hex".parse::<TraceId>().is_err());
982 assert!("not-hex".parse::<DecisionId>().is_err());
983 }
984
985 #[test]
986 fn invalid_version_parse_fails() {
987 assert!("1.2".parse::<SchemaVersion>().is_err());
988 assert!("a.b.c".parse::<SchemaVersion>().is_err());
989 assert!("1.2.3.4".parse::<SchemaVersion>().is_err());
990 assert!("".parse::<SchemaVersion>().is_err());
991 }
992
993 #[test]
998 fn all_types_send_sync() {
999 fn assert_send_sync<T: Send + Sync>() {}
1000 assert_send_sync::<TraceId>();
1001 assert_send_sync::<DecisionId>();
1002 assert_send_sync::<PolicyId>();
1003 assert_send_sync::<SchemaVersion>();
1004 assert_send_sync::<Budget>();
1005 assert_send_sync::<NoCaps>();
1006 assert_send_sync::<Cx<'_, NoCaps>>();
1008 }
1009
1010 #[test]
1015 fn budget_new_and_remaining() {
1016 let b = Budget::new(5000);
1017 assert_eq!(b.remaining_ms(), 5000);
1018 assert!(!b.is_exhausted());
1019 }
1020
1021 #[test]
1022 fn budget_consume() {
1023 let b = Budget::new(1000);
1024 let b2 = b.consume(300).unwrap();
1025 assert_eq!(b2.remaining_ms(), 700);
1026 let b3 = b2.consume(700).unwrap();
1027 assert_eq!(b3.remaining_ms(), 0);
1028 assert!(b3.is_exhausted());
1029 }
1030
1031 #[test]
1032 fn budget_consume_insufficient() {
1033 let b = Budget::new(100);
1034 assert!(b.consume(200).is_none());
1035 }
1036
1037 #[test]
1038 fn budget_consume_exact() {
1039 let b = Budget::new(100);
1040 let b2 = b.consume(100).unwrap();
1041 assert!(b2.is_exhausted());
1042 }
1043
1044 #[test]
1045 fn budget_consume_zero() {
1046 let b = Budget::new(100);
1047 let b2 = b.consume(0).unwrap();
1048 assert_eq!(b2.remaining_ms(), 100);
1049 }
1050
1051 #[test]
1052 fn budget_min() {
1053 let b1 = Budget::new(500);
1054 let b2 = Budget::new(300);
1055 assert_eq!(b1.min(b2).remaining_ms(), 300);
1056 assert_eq!(b2.min(b1).remaining_ms(), 300);
1057 }
1058
1059 #[test]
1060 fn budget_min_equal() {
1061 let b = Budget::new(100);
1062 assert_eq!(b.min(b).remaining_ms(), 100);
1063 }
1064
1065 #[test]
1066 fn budget_unlimited() {
1067 let b = Budget::UNLIMITED;
1068 assert_eq!(b.remaining_ms(), u64::MAX);
1069 assert!(!b.is_exhausted());
1070 }
1071
1072 #[test]
1073 fn budget_unlimited_min_with_finite() {
1074 let finite = Budget::new(1000);
1075 assert_eq!(Budget::UNLIMITED.min(finite).remaining_ms(), 1000);
1076 assert_eq!(finite.min(Budget::UNLIMITED).remaining_ms(), 1000);
1077 }
1078
1079 #[test]
1080 fn budget_serde_json() {
1081 let b = Budget::new(42);
1082 let json = serde_json::to_string(&b).unwrap();
1083 let parsed: Budget = serde_json::from_str(&json).unwrap();
1084 assert_eq!(b, parsed);
1085 }
1086
1087 #[test]
1088 fn budget_copy_semantics() {
1089 let b = Budget::new(100);
1090 let copy = b;
1091 assert_eq!(b, copy);
1092 }
1093
1094 #[test]
1095 fn budget_tropical_identity() {
1096 let b = Budget::new(42);
1098 assert_eq!(b.min(Budget::UNLIMITED), b);
1099 assert_eq!(Budget::UNLIMITED.min(b), b);
1100 }
1101
1102 #[test]
1103 fn budget_tropical_commutativity() {
1104 let a = Budget::new(100);
1105 let b = Budget::new(200);
1106 assert_eq!(a.min(b), b.min(a));
1107 }
1108
1109 #[test]
1110 fn budget_tropical_associativity() {
1111 let a = Budget::new(100);
1112 let b = Budget::new(200);
1113 let c = Budget::new(50);
1114 assert_eq!(a.min(b).min(c), a.min(b.min(c)));
1115 }
1116
1117 #[test]
1122 fn no_caps_empty() {
1123 let caps = NoCaps;
1124 assert_eq!(caps.count(), 0);
1125 assert!(caps.is_empty());
1126 assert!(caps.capability_names().is_empty());
1127 }
1128
1129 #[test]
1130 fn no_caps_clone() {
1131 let a = NoCaps;
1132 let b = a.clone();
1133 assert_eq!(a, b);
1134 }
1135
1136 #[derive(Clone, Debug)]
1141 struct TestCaps {
1142 can_read: bool,
1143 can_write: bool,
1144 }
1145
1146 impl CapabilitySet for TestCaps {
1147 fn capability_names(&self) -> alloc::vec::Vec<&str> {
1148 let mut names = alloc::vec::Vec::new();
1149 if self.can_read {
1150 names.push("read");
1151 }
1152 if self.can_write {
1153 names.push("write");
1154 }
1155 names
1156 }
1157
1158 fn count(&self) -> usize {
1159 usize::from(self.can_read) + usize::from(self.can_write)
1160 }
1161 }
1162
1163 #[derive(Clone, Debug)]
1165 struct LayeredCaps {
1166 level: u32,
1167 }
1168
1169 impl CapabilitySet for LayeredCaps {
1170 fn capability_names(&self) -> alloc::vec::Vec<&str> {
1171 if self.level > 0 {
1172 alloc::vec!["layer"]
1173 } else {
1174 alloc::vec::Vec::new()
1175 }
1176 }
1177
1178 fn count(&self) -> usize {
1179 usize::from(self.level > 0)
1180 }
1181 }
1182
1183 #[test]
1188 fn cx_root_creation() {
1189 let trace = TraceId::from_parts(1_700_000_000_000, 1);
1190 let cx = Cx::new(trace, Budget::new(5000), NoCaps);
1191 assert_eq!(cx.trace_id(), trace);
1192 assert_eq!(cx.budget().remaining_ms(), 5000);
1193 assert_eq!(cx.depth(), 0);
1194 assert!(cx.capabilities().is_empty());
1195 }
1196
1197 #[test]
1198 fn cx_child_inherits_trace() {
1199 let trace = TraceId::from_parts(1_700_000_000_000, 42);
1200 let cx = Cx::new(trace, Budget::new(5000), NoCaps);
1201 let child = cx.child(NoCaps, Budget::new(3000));
1202 assert_eq!(child.trace_id(), trace);
1203 }
1204
1205 #[test]
1206 fn cx_child_budget_takes_min() {
1207 let cx = Cx::new(TraceId::from_raw(1), Budget::new(2000), NoCaps);
1208 let child1 = cx.child(NoCaps, Budget::new(1000));
1209 assert_eq!(child1.budget().remaining_ms(), 1000);
1210 let child2 = cx.child(NoCaps, Budget::new(5000));
1211 assert_eq!(child2.budget().remaining_ms(), 2000);
1212 }
1213
1214 #[test]
1215 fn cx_child_increments_depth() {
1216 let cx = Cx::new(TraceId::from_raw(1), Budget::new(1000), NoCaps);
1217 let child = cx.child(NoCaps, Budget::new(1000));
1218 assert_eq!(child.depth(), 1);
1219 let grandchild = child.child(NoCaps, Budget::new(1000));
1220 assert_eq!(grandchild.depth(), 2);
1221 }
1222
1223 #[test]
1224 fn cx_consume_budget() {
1225 let mut cx = Cx::new(TraceId::from_raw(1), Budget::new(500), NoCaps);
1226 assert!(cx.consume_budget(200));
1227 assert_eq!(cx.budget().remaining_ms(), 300);
1228 assert!(!cx.consume_budget(400));
1229 assert_eq!(cx.budget().remaining_ms(), 300);
1230 }
1231
1232 #[test]
1233 fn cx_debug_format() {
1234 let cx = Cx::new(TraceId::from_raw(0xAB), Budget::new(100), NoCaps);
1235 let dbg = std::format!("{cx:?}");
1236 assert!(dbg.contains("Cx"));
1237 assert!(dbg.contains("budget_ms"));
1238 assert!(dbg.contains("100"));
1239 }
1240
1241 #[test]
1242 fn cx_with_custom_capabilities() {
1243 let caps = TestCaps {
1244 can_read: true,
1245 can_write: false,
1246 };
1247 let cx = Cx::new(TraceId::from_raw(1), Budget::new(1000), caps);
1248 assert_eq!(cx.capabilities().count(), 1);
1249 assert_eq!(cx.capabilities().capability_names(), &["read"]);
1250 }
1251
1252 #[test]
1253 fn cx_child_with_attenuated_capabilities() {
1254 let full_caps = TestCaps {
1255 can_read: true,
1256 can_write: true,
1257 };
1258 let cx = Cx::new(TraceId::from_raw(1), Budget::new(1000), full_caps);
1259 assert_eq!(cx.capabilities().count(), 2);
1260
1261 let read_only = TestCaps {
1262 can_read: true,
1263 can_write: false,
1264 };
1265 let child = cx.child(read_only, Budget::new(500));
1266 assert_eq!(child.capabilities().count(), 1);
1267 assert!(!child.capabilities().capability_names().contains(&"write"));
1268 }
1269
1270 #[test]
1271 fn cx_capability_attenuation_chain_10x() {
1272 let trace = TraceId::from_raw(0x42);
1274 let root = Cx::new(trace, Budget::new(10_000), LayeredCaps { level: 10 });
1275 assert_eq!(root.capabilities().level, 10);
1276
1277 let mut prev_level = 10_u32;
1278 let child1 = root.child(LayeredCaps { level: 9 }, Budget::new(9000));
1279 assert!(child1.capabilities().level < prev_level);
1280 prev_level = child1.capabilities().level;
1281
1282 let child2 = child1.child(LayeredCaps { level: 8 }, Budget::new(8000));
1283 assert!(child2.capabilities().level < prev_level);
1284 prev_level = child2.capabilities().level;
1285
1286 let child3 = child2.child(LayeredCaps { level: 7 }, Budget::new(7000));
1287 assert!(child3.capabilities().level < prev_level);
1288 prev_level = child3.capabilities().level;
1289
1290 let child4 = child3.child(LayeredCaps { level: 6 }, Budget::new(6000));
1291 assert!(child4.capabilities().level < prev_level);
1292 prev_level = child4.capabilities().level;
1293
1294 let child5 = child4.child(LayeredCaps { level: 5 }, Budget::new(5000));
1295 assert!(child5.capabilities().level < prev_level);
1296 prev_level = child5.capabilities().level;
1297
1298 let child6 = child5.child(LayeredCaps { level: 4 }, Budget::new(4000));
1299 assert!(child6.capabilities().level < prev_level);
1300 prev_level = child6.capabilities().level;
1301
1302 let child7 = child6.child(LayeredCaps { level: 3 }, Budget::new(3000));
1303 assert!(child7.capabilities().level < prev_level);
1304 prev_level = child7.capabilities().level;
1305
1306 let child8 = child7.child(LayeredCaps { level: 2 }, Budget::new(2000));
1307 assert!(child8.capabilities().level < prev_level);
1308 prev_level = child8.capabilities().level;
1309
1310 let child9 = child8.child(LayeredCaps { level: 1 }, Budget::new(1000));
1311 assert!(child9.capabilities().level < prev_level);
1312 prev_level = child9.capabilities().level;
1313
1314 let child10 = child9.child(LayeredCaps { level: 0 }, Budget::new(500));
1315 assert!(child10.capabilities().level < prev_level);
1316 assert_eq!(child10.capabilities().level, 0);
1317 assert!(child10.capabilities().is_empty());
1318 assert_eq!(child10.depth(), 10);
1319
1320 assert_eq!(child10.trace_id(), trace);
1322 assert_eq!(child10.budget().remaining_ms(), 500);
1324 }
1325
1326 #[test]
1327 fn cx_deep_nesting_budget_monotonic() {
1328 let cx = Cx::new(TraceId::from_raw(1), Budget::new(1000), NoCaps);
1330 let c1 = cx.child(NoCaps, Budget::new(900));
1331 let c2 = c1.child(NoCaps, Budget::new(800));
1332 let c3 = c2.child(NoCaps, Budget::new(700));
1333 let c4 = c3.child(NoCaps, Budget::new(600));
1334
1335 assert!(c1.budget().remaining_ms() <= cx.budget().remaining_ms());
1336 assert!(c2.budget().remaining_ms() <= c1.budget().remaining_ms());
1337 assert!(c3.budget().remaining_ms() <= c2.budget().remaining_ms());
1338 assert!(c4.budget().remaining_ms() <= c3.budget().remaining_ms());
1339 }
1340
1341 #[test]
1342 fn cx_child_cannot_exceed_parent_budget() {
1343 let cx = Cx::new(TraceId::from_raw(1), Budget::new(100), NoCaps);
1344 let child = cx.child(NoCaps, Budget::UNLIMITED);
1346 assert_eq!(child.budget().remaining_ms(), 100);
1347 }
1348
1349 #[test]
1350 fn cx_trace_propagation_through_chain() {
1351 let trace = TraceId::from_parts(1_700_000_000_000, 0xCAFE);
1352 let cx = Cx::new(trace, Budget::UNLIMITED, NoCaps);
1353 let c1 = cx.child(NoCaps, Budget::UNLIMITED);
1354 let c2 = c1.child(NoCaps, Budget::UNLIMITED);
1355 let c3 = c2.child(NoCaps, Budget::UNLIMITED);
1356 assert_eq!(c3.trace_id(), trace);
1357 assert_eq!(c3.depth(), 3);
1358 }
1359}
1360
1361#[cfg(test)]
1366mod proptest_tests {
1367 extern crate std;
1368
1369 use super::*;
1370 use core::hash::{Hash, Hasher};
1371 use proptest::prelude::*;
1372 use std::collections::hash_map::DefaultHasher;
1373 use std::string::ToString;
1374
1375 fn hash_of<T: Hash>(val: &T) -> u64 {
1376 let mut h = DefaultHasher::new();
1377 val.hash(&mut h);
1378 h.finish()
1379 }
1380
1381 proptest! {
1384 #[test]
1385 fn trace_id_display_fromstr_roundtrip(raw: u128) {
1386 let id = TraceId::from_raw(raw);
1387 let hex = id.to_string();
1388 let parsed: TraceId = hex.parse().unwrap();
1389 prop_assert_eq!(id, parsed);
1390 }
1391
1392 #[test]
1393 fn trace_id_serde_roundtrip(raw: u128) {
1394 let id = TraceId::from_raw(raw);
1395 let json = serde_json::to_string(&id).unwrap();
1396 let parsed: TraceId = serde_json::from_str(&json).unwrap();
1397 prop_assert_eq!(id, parsed);
1398 }
1399
1400 #[test]
1401 fn trace_id_bytes_roundtrip(raw: u128) {
1402 let id = TraceId::from_raw(raw);
1403 let bytes = id.to_bytes();
1404 let recovered = TraceId::from_bytes(bytes);
1405 prop_assert_eq!(id, recovered);
1406 }
1407
1408 #[test]
1409 fn trace_id_hash_consistency(a: u128, b: u128) {
1410 let id_a = TraceId::from_raw(a);
1411 let id_b = TraceId::from_raw(b);
1412 if id_a == id_b {
1413 prop_assert_eq!(hash_of(&id_a), hash_of(&id_b));
1414 }
1415 }
1416
1417 #[test]
1418 fn trace_id_from_parts_preserves_timestamp(ts_ms: u64, random: u128) {
1419 let ts_masked = ts_ms & 0xFFFF_FFFF_FFFF;
1421 let id = TraceId::from_parts(ts_masked, random);
1422 prop_assert_eq!(id.timestamp_ms(), ts_masked);
1423 }
1424 }
1425
1426 proptest! {
1429 #[test]
1430 fn decision_id_display_fromstr_roundtrip(raw: u128) {
1431 let id = DecisionId::from_raw(raw);
1432 let hex = id.to_string();
1433 let parsed: DecisionId = hex.parse().unwrap();
1434 prop_assert_eq!(id, parsed);
1435 }
1436
1437 #[test]
1438 fn decision_id_serde_roundtrip(raw: u128) {
1439 let id = DecisionId::from_raw(raw);
1440 let json = serde_json::to_string(&id).unwrap();
1441 let parsed: DecisionId = serde_json::from_str(&json).unwrap();
1442 prop_assert_eq!(id, parsed);
1443 }
1444
1445 #[test]
1446 fn decision_id_hash_consistency(a: u128, b: u128) {
1447 let id_a = DecisionId::from_raw(a);
1448 let id_b = DecisionId::from_raw(b);
1449 if id_a == id_b {
1450 prop_assert_eq!(hash_of(&id_a), hash_of(&id_b));
1451 }
1452 }
1453 }
1454
1455 proptest! {
1458 #[test]
1459 fn schema_version_parse_roundtrip(major: u32, minor: u32, patch: u32) {
1460 let v = SchemaVersion::new(major, minor, patch);
1461 let s = v.to_string();
1462 let parsed: SchemaVersion = s.parse().unwrap();
1463 prop_assert_eq!(v, parsed);
1464 }
1465
1466 #[test]
1467 fn schema_version_serde_roundtrip(major: u32, minor: u32, patch: u32) {
1468 let v = SchemaVersion::new(major, minor, patch);
1469 let json = serde_json::to_string(&v).unwrap();
1470 let parsed: SchemaVersion = serde_json::from_str(&json).unwrap();
1471 prop_assert_eq!(v, parsed);
1472 }
1473
1474 #[test]
1475 fn schema_version_compatible_reflexive(major: u32, minor: u32, patch: u32) {
1476 let v = SchemaVersion::new(major, minor, patch);
1477 prop_assert!(v.is_compatible(&v));
1478 }
1479
1480 #[test]
1481 fn schema_version_compatible_symmetric(
1482 m1: u32, n1: u32, p1: u32,
1483 m2: u32, n2: u32, p2: u32
1484 ) {
1485 let a = SchemaVersion::new(m1, n1, p1);
1486 let b = SchemaVersion::new(m2, n2, p2);
1487 prop_assert_eq!(a.is_compatible(&b), b.is_compatible(&a));
1488 }
1489
1490 #[test]
1491 fn schema_version_compatible_transitive(
1492 m1: u32, n1: u32, p1: u32,
1493 n2: u32, p2: u32,
1494 n3: u32, p3: u32
1495 ) {
1496 let a = SchemaVersion::new(m1, n1, p1);
1499 let b = SchemaVersion::new(m1, n2, p2);
1500 let c = SchemaVersion::new(m1, n3, p3);
1501 if a.is_compatible(&b) && b.is_compatible(&c) {
1502 prop_assert!(a.is_compatible(&c));
1503 }
1504 }
1505
1506 #[test]
1507 fn schema_version_hash_consistency(
1508 m1: u32, n1: u32, p1: u32,
1509 m2: u32, n2: u32, p2: u32
1510 ) {
1511 let a = SchemaVersion::new(m1, n1, p1);
1512 let b = SchemaVersion::new(m2, n2, p2);
1513 if a == b {
1514 prop_assert_eq!(hash_of(&a), hash_of(&b));
1515 }
1516 }
1517 }
1518
1519 proptest! {
1522 #[test]
1523 fn budget_min_commutative(a: u64, b: u64) {
1524 let ba = Budget::new(a);
1525 let bb = Budget::new(b);
1526 prop_assert_eq!(ba.min(bb), bb.min(ba));
1527 }
1528
1529 #[test]
1530 fn budget_min_associative(a: u64, b: u64, c: u64) {
1531 let ba = Budget::new(a);
1532 let bb = Budget::new(b);
1533 let bc = Budget::new(c);
1534 prop_assert_eq!(ba.min(bb).min(bc), ba.min(bb.min(bc)));
1535 }
1536
1537 #[test]
1538 fn budget_min_identity(a: u64) {
1539 let ba = Budget::new(a);
1541 prop_assert_eq!(ba.min(Budget::UNLIMITED), ba);
1542 prop_assert_eq!(Budget::UNLIMITED.min(ba), ba);
1543 }
1544
1545 #[test]
1546 fn budget_min_idempotent(a: u64) {
1547 let ba = Budget::new(a);
1548 prop_assert_eq!(ba.min(ba), ba);
1549 }
1550
1551 #[test]
1552 fn budget_consume_additive(total in 0..=10_000_u64, a in 0..=5_000_u64, b in 0..=5_000_u64) {
1553 let budget = Budget::new(total);
1555 if a + b <= total {
1556 let after_both = budget.consume(a + b).unwrap();
1557 let after_a = budget.consume(a).unwrap();
1558 let after_ab = after_a.consume(b).unwrap();
1559 prop_assert_eq!(after_both.remaining_ms(), after_ab.remaining_ms());
1560 }
1561 }
1562
1563 #[test]
1564 fn budget_serde_roundtrip(ms: u64) {
1565 let b = Budget::new(ms);
1566 let json = serde_json::to_string(&b).unwrap();
1567 let parsed: Budget = serde_json::from_str(&json).unwrap();
1568 prop_assert_eq!(b, parsed);
1569 }
1570
1571 #[test]
1572 fn budget_hash_consistency(a: u64, b: u64) {
1573 let ba = Budget::new(a);
1574 let bb = Budget::new(b);
1575 if ba == bb {
1576 prop_assert_eq!(hash_of(&ba), hash_of(&bb));
1577 }
1578 }
1579 }
1580
1581 proptest! {
1584 #[test]
1585 fn cx_child_budget_never_exceeds_parent(parent_ms: u64, child_ms: u64) {
1586 let trace = TraceId::from_raw(1);
1587 let cx = Cx::new(trace, Budget::new(parent_ms), NoCaps);
1588 let child = cx.child(NoCaps, Budget::new(child_ms));
1589 prop_assert!(child.budget().remaining_ms() <= cx.budget().remaining_ms());
1590 }
1591
1592 #[test]
1593 fn cx_child_trace_always_inherited(raw: u128, budget_ms: u64) {
1594 let trace = TraceId::from_raw(raw);
1595 let cx = Cx::new(trace, Budget::new(budget_ms), NoCaps);
1596 let child = cx.child(NoCaps, Budget::new(budget_ms));
1597 prop_assert_eq!(child.trace_id(), trace);
1598 }
1599
1600 #[test]
1601 fn cx_child_depth_increments(raw: u128, budget_ms: u64) {
1602 let cx = Cx::new(TraceId::from_raw(raw), Budget::new(budget_ms), NoCaps);
1603 let child = cx.child(NoCaps, Budget::new(budget_ms));
1604 prop_assert_eq!(child.depth(), cx.depth() + 1);
1605 }
1606 }
1607}