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