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]
100 pub const fn from_parts(ts_ms: u64, random: u128) -> Self {
101 const MAX_TS_MS: u64 = (1u64 << 48) - 1;
102 let saturated_ts = if ts_ms > MAX_TS_MS { MAX_TS_MS } else { ts_ms };
103 let ts_bits = (saturated_ts as u128) << 80;
104 let rand_bits = random & 0xFFFF_FFFF_FFFF_FFFF_FFFF; Self(ts_bits | rand_bits)
106 }
107
108 pub const fn try_from_parts(ts_ms: u64, random: u128) -> Result<Self, u64> {
119 const MAX_TS_MS: u64 = (1u64 << 48) - 1;
120 if ts_ms > MAX_TS_MS {
121 return Err(ts_ms);
122 }
123 let ts_bits = (ts_ms as u128) << 80;
124 let rand_bits = random & 0xFFFF_FFFF_FFFF_FFFF_FFFF;
125 Ok(Self(ts_bits | rand_bits))
126 }
127
128 #[must_use]
130 pub const fn timestamp_ms(self) -> u64 {
131 (self.0 >> 80) as u64
132 }
133
134 #[must_use]
136 pub const fn as_u128(self) -> u128 {
137 self.0
138 }
139
140 #[must_use]
142 pub const fn to_bytes(self) -> [u8; 16] {
143 self.0.to_be_bytes()
144 }
145
146 #[must_use]
148 pub const fn from_bytes(bytes: [u8; 16]) -> Self {
149 Self(u128::from_be_bytes(bytes))
150 }
151}
152
153impl fmt::Debug for TraceId {
154 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155 write!(f, "TraceId({:032x})", self.0)
156 }
157}
158
159impl fmt::Display for TraceId {
160 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161 write!(f, "{:032x}", self.0)
162 }
163}
164
165impl FromStr for TraceId {
166 type Err = ParseIdError;
167
168 fn from_str(s: &str) -> Result<Self, Self::Err> {
169 let val = u128::from_str_radix(s, 16).map_err(|_| ParseIdError {
170 kind: "TraceId",
171 input_len: s.len(),
172 })?;
173 Ok(Self(val))
174 }
175}
176
177#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
185#[serde(transparent)]
186pub struct DecisionId(#[serde(with = "hex_u128")] u128);
187
188impl DecisionId {
189 #[must_use]
191 pub const fn from_raw(raw: u128) -> Self {
192 Self(raw)
193 }
194
195 #[must_use]
206 pub const fn from_parts(ts_ms: u64, random: u128) -> Self {
207 const MAX_TS_MS: u64 = (1u64 << 48) - 1;
208 let saturated_ts = if ts_ms > MAX_TS_MS { MAX_TS_MS } else { ts_ms };
209 let ts_bits = (saturated_ts as u128) << 80;
210 let rand_bits = random & 0xFFFF_FFFF_FFFF_FFFF_FFFF;
211 Self(ts_bits | rand_bits)
212 }
213
214 pub const fn try_from_parts(ts_ms: u64, random: u128) -> Result<Self, u64> {
223 const MAX_TS_MS: u64 = (1u64 << 48) - 1;
224 if ts_ms > MAX_TS_MS {
225 return Err(ts_ms);
226 }
227 let ts_bits = (ts_ms as u128) << 80;
228 let rand_bits = random & 0xFFFF_FFFF_FFFF_FFFF_FFFF;
229 Ok(Self(ts_bits | rand_bits))
230 }
231
232 #[must_use]
234 pub const fn timestamp_ms(self) -> u64 {
235 (self.0 >> 80) as u64
236 }
237
238 #[must_use]
240 pub const fn as_u128(self) -> u128 {
241 self.0
242 }
243
244 #[must_use]
246 pub const fn to_bytes(self) -> [u8; 16] {
247 self.0.to_be_bytes()
248 }
249
250 pub const fn from_bytes(bytes: [u8; 16]) -> Self {
252 Self(u128::from_be_bytes(bytes))
253 }
254}
255
256impl fmt::Debug for DecisionId {
257 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258 write!(f, "DecisionId({:032x})", self.0)
259 }
260}
261
262impl fmt::Display for DecisionId {
263 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
264 write!(f, "{:032x}", self.0)
265 }
266}
267
268impl FromStr for DecisionId {
269 type Err = ParseIdError;
270
271 fn from_str(s: &str) -> Result<Self, Self::Err> {
272 let val = u128::from_str_radix(s, 16).map_err(|_| ParseIdError {
273 kind: "DecisionId",
274 input_len: s.len(),
275 })?;
276 Ok(Self(val))
277 }
278}
279
280#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
296pub struct PolicyId {
297 #[serde(rename = "n")]
299 name: String,
300 #[serde(rename = "v")]
302 version: u32,
303}
304
305impl PolicyId {
306 pub fn new(name: impl Into<String>, version: u32) -> Self {
308 Self {
309 name: name.into(),
310 version,
311 }
312 }
313
314 pub fn name(&self) -> &str {
316 &self.name
317 }
318
319 pub const fn version(&self) -> u32 {
321 self.version
322 }
323}
324
325impl fmt::Display for PolicyId {
326 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
327 write!(f, "{}@v{}", self.name, self.version)
328 }
329}
330
331#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
350pub struct SchemaVersion {
351 pub major: u32,
353 pub minor: u32,
355 pub patch: u32,
357}
358
359impl SchemaVersion {
360 pub const fn new(major: u32, minor: u32, patch: u32) -> Self {
362 Self {
363 major,
364 minor,
365 patch,
366 }
367 }
368
369 pub const fn is_compatible(&self, other: &Self) -> bool {
371 self.major == other.major
372 }
373}
374
375impl fmt::Display for SchemaVersion {
376 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
377 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
378 }
379}
380
381impl FromStr for SchemaVersion {
382 type Err = ParseVersionError;
383
384 fn from_str(s: &str) -> Result<Self, Self::Err> {
385 let parts: alloc::vec::Vec<&str> = s.split('.').collect();
386 if parts.len() != 3 {
387 return Err(ParseVersionError);
388 }
389 let major = parts[0].parse().map_err(|_| ParseVersionError)?;
390 let minor = parts[1].parse().map_err(|_| ParseVersionError)?;
391 let patch = parts[2].parse().map_err(|_| ParseVersionError)?;
392 Ok(Self {
393 major,
394 minor,
395 patch,
396 })
397 }
398}
399
400#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
418pub struct Budget {
419 remaining_ms: u64,
420}
421
422impl Budget {
423 pub const fn new(ms: u64) -> Self {
425 Self { remaining_ms: ms }
426 }
427
428 pub const fn remaining_ms(self) -> u64 {
430 self.remaining_ms
431 }
432
433 pub const fn consume(self, ms: u64) -> Option<Self> {
437 if self.remaining_ms >= ms {
438 Some(Self {
439 remaining_ms: self.remaining_ms - ms,
440 })
441 } else {
442 None
443 }
444 }
445
446 pub const fn is_exhausted(self) -> bool {
448 self.remaining_ms == 0
449 }
450
451 #[must_use]
453 pub const fn min(self, other: Self) -> Self {
454 if self.remaining_ms <= other.remaining_ms {
455 self
456 } else {
457 other
458 }
459 }
460
461 pub const UNLIMITED: Self = Self {
463 remaining_ms: u64::MAX,
464 };
465}
466
467pub trait CapabilitySet: Clone + fmt::Debug + Send + Sync {
480 fn capability_names(&self) -> Vec<&str>;
482
483 fn count(&self) -> usize;
485
486 fn is_empty(&self) -> bool {
488 self.count() == 0
489 }
490}
491
492#[derive(Clone, Debug, Default, PartialEq, Eq)]
494pub struct NoCaps;
495
496impl CapabilitySet for NoCaps {
497 fn capability_names(&self) -> Vec<&str> {
498 Vec::new()
499 }
500
501 fn count(&self) -> usize {
502 0
503 }
504}
505
506pub struct Cx<'a, C: CapabilitySet = NoCaps> {
529 trace_id: TraceId,
530 budget: Budget,
531 capabilities: C,
532 depth: u32,
533 _scope: PhantomData<&'a ()>,
534}
535
536impl<C: CapabilitySet> Cx<'_, C> {
537 pub fn new(trace_id: TraceId, budget: Budget, capabilities: C) -> Self {
539 Self {
540 trace_id,
541 budget,
542 capabilities,
543 depth: 0,
544 _scope: PhantomData,
545 }
546 }
547
548 pub fn child(&self, capabilities: C, budget: Budget) -> Cx<'_, C> {
553 Cx {
554 trace_id: self.trace_id,
555 budget: self.budget.min(budget),
556 capabilities,
557 depth: self.depth + 1,
558 _scope: PhantomData,
559 }
560 }
561
562 pub const fn trace_id(&self) -> TraceId {
564 self.trace_id
565 }
566
567 pub const fn budget(&self) -> Budget {
569 self.budget
570 }
571
572 pub fn capabilities(&self) -> &C {
574 &self.capabilities
575 }
576
577 pub const fn depth(&self) -> u32 {
579 self.depth
580 }
581
582 pub fn consume_budget(&mut self, ms: u64) -> bool {
586 match self.budget.consume(ms) {
587 Some(new_budget) => {
588 self.budget = new_budget;
589 true
590 }
591 None => false,
592 }
593 }
594}
595
596impl<C: CapabilitySet> fmt::Debug for Cx<'_, C> {
597 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
598 f.debug_struct("Cx")
599 .field("trace_id", &self.trace_id)
600 .field("budget_ms", &self.budget.remaining_ms())
601 .field("capabilities", &self.capabilities)
602 .field("depth", &self.depth)
603 .finish()
604 }
605}
606
607#[derive(Clone, Debug, PartialEq, Eq)]
613pub struct ParseIdError {
614 pub kind: &'static str,
616 pub input_len: usize,
618}
619
620impl fmt::Display for ParseIdError {
621 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
622 write!(
623 f,
624 "invalid {} hex string (length {})",
625 self.kind, self.input_len
626 )
627 }
628}
629
630#[derive(Clone, Debug, PartialEq, Eq)]
632pub struct ParseVersionError;
633
634impl fmt::Display for ParseVersionError {
635 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
636 write!(f, "invalid schema version (expected major.minor.patch)")
637 }
638}
639
640mod hex_u128 {
645 use alloc::format;
646 use alloc::string::String;
647
648 use serde::{self, Deserialize, Deserializer, Serializer};
649
650 pub fn serialize<S>(value: &u128, serializer: S) -> Result<S::Ok, S::Error>
651 where
652 S: Serializer,
653 {
654 serializer.serialize_str(&format!("{value:032x}"))
655 }
656
657 pub fn deserialize<'de, D>(deserializer: D) -> Result<u128, D::Error>
658 where
659 D: Deserializer<'de>,
660 {
661 let s = String::deserialize(deserializer)?;
662 u128::from_str_radix(&s, 16)
663 .map_err(|_| serde::de::Error::custom(format!("invalid hex u128: {s}")))
664 }
665}
666
667#[cfg(test)]
672mod tests {
673 extern crate std;
674
675 use super::*;
676 use core::hash::{Hash, Hasher};
677 use std::collections::hash_map::DefaultHasher;
678 use std::string::ToString;
679
680 fn hash_of<T: Hash>(val: &T) -> u64 {
681 let mut h = DefaultHasher::new();
682 val.hash(&mut h);
683 h.finish()
684 }
685
686 #[test]
691 fn trace_id_from_parts_roundtrip() {
692 let ts = 1_700_000_000_000_u64;
693 let random = 0x00AB_CDEF_0123_4567_89AB_u128;
694 let id = TraceId::from_parts(ts, random);
695 assert_eq!(id.timestamp_ms(), ts);
696 assert_eq!(id.as_u128() & 0xFFFF_FFFF_FFFF_FFFF_FFFF, random);
697 }
698
699 #[test]
700 fn decision_id_from_parts_saturates_out_of_range_ts() {
701 const MAX_TS_MS: u64 = (1u64 << 48) - 1;
704 let random = 0x00AB_CDEF_0123_4567_89AB_u128;
705
706 let in_range = DecisionId::from_parts(1_700_000_000_000_u64, random);
708 assert_eq!(in_range.timestamp_ms(), 1_700_000_000_000_u64);
709 assert_eq!(in_range.as_u128() & 0xFFFF_FFFF_FFFF_FFFF_FFFF, random);
710
711 let out_of_range_ts = (1u64 << 48) + 12_345;
714 let saturated = DecisionId::from_parts(out_of_range_ts, random);
715 assert_eq!(saturated.timestamp_ms(), MAX_TS_MS);
716 assert_eq!(saturated.as_u128() & 0xFFFF_FFFF_FFFF_FFFF_FFFF, random);
717
718 assert!(matches!(
720 DecisionId::try_from_parts(out_of_range_ts, random),
721 Err(t) if t == out_of_range_ts
722 ));
723 assert!(DecisionId::try_from_parts(MAX_TS_MS, random).is_ok());
724 }
725
726 #[test]
727 fn trace_id_display_parse_roundtrip() {
728 let id = TraceId::from_raw(0x0123_4567_89AB_CDEF_0123_4567_89AB_CDEF);
729 let hex = id.to_string();
730 assert_eq!(hex, "0123456789abcdef0123456789abcdef");
731 let parsed: TraceId = hex.parse().unwrap();
732 assert_eq!(id, parsed);
733 }
734
735 #[test]
736 fn trace_id_bytes_roundtrip() {
737 let id = TraceId::from_raw(42);
738 let bytes = id.to_bytes();
739 let recovered = TraceId::from_bytes(bytes);
740 assert_eq!(id, recovered);
741 }
742
743 #[test]
744 fn trace_id_ordering() {
745 let earlier = TraceId::from_parts(1000, 0);
746 let later = TraceId::from_parts(2000, 0);
747 assert!(earlier < later);
748 }
749
750 #[test]
751 fn trace_id_uuidv7_monotonic_ordering_10k() {
752 let ids: std::vec::Vec<TraceId> = (0..10_000)
754 .map(|i| TraceId::from_parts(1_700_000_000_000 + i, 0))
755 .collect();
756 for window in ids.windows(2) {
757 assert!(
758 window[0] < window[1],
759 "TraceId ordering violated: {:?} >= {:?}",
760 window[0],
761 window[1]
762 );
763 }
764 }
765
766 #[test]
767 fn trace_id_display_parse_roundtrip_many() {
768 for i in 0..10_000_u128 {
770 let raw = i.wrapping_mul(0x0123_4567_89AB_CDEF) ^ (i << 64);
771 let id = TraceId::from_raw(raw);
772 let hex = id.to_string();
773 let parsed: TraceId = hex.parse().unwrap();
774 assert_eq!(id, parsed, "roundtrip failed for raw={raw:#034x}");
775 }
776 }
777
778 #[test]
779 fn trace_id_serde_json() {
780 let id = TraceId::from_raw(0xFF);
781 let json = serde_json::to_string(&id).unwrap();
782 assert_eq!(json, "\"000000000000000000000000000000ff\"");
783 let parsed: TraceId = serde_json::from_str(&json).unwrap();
784 assert_eq!(id, parsed);
785 }
786
787 #[test]
788 fn trace_id_serde_roundtrip_many() {
789 for i in 0..1_000_u128 {
790 let id = TraceId::from_raw(i.wrapping_mul(0xDEAD_BEEF_CAFE_1234));
791 let json = serde_json::to_string(&id).unwrap();
792 let parsed: TraceId = serde_json::from_str(&json).unwrap();
793 assert_eq!(id, parsed);
794 }
795 }
796
797 #[test]
798 fn trace_id_debug_format() {
799 let id = TraceId::from_raw(0xAB);
800 let dbg = std::format!("{id:?}");
801 assert!(dbg.starts_with("TraceId("));
802 assert!(dbg.contains("ab"));
803 }
804
805 #[test]
806 fn trace_id_copy_semantics() {
807 let id = TraceId::from_raw(42);
808 let copy = id;
809 assert_eq!(id, copy); }
811
812 #[test]
813 fn trace_id_hash_consistency() {
814 let a = TraceId::from_raw(0xDEAD);
815 let b = TraceId::from_raw(0xDEAD);
816 assert_eq!(a, b);
817 assert_eq!(hash_of(&a), hash_of(&b));
818 }
819
820 #[test]
821 fn trace_id_zero_and_max() {
822 let zero = TraceId::from_raw(0);
823 assert_eq!(zero.timestamp_ms(), 0);
824 assert_eq!(zero.to_string(), "00000000000000000000000000000000");
825 let roundtrip: TraceId = zero.to_string().parse().unwrap();
826 assert_eq!(zero, roundtrip);
827
828 let max = TraceId::from_raw(u128::MAX);
829 assert_eq!(max.to_string(), "ffffffffffffffffffffffffffffffff");
830 let roundtrip: TraceId = max.to_string().parse().unwrap();
831 assert_eq!(max, roundtrip);
832 }
833
834 #[test]
839 fn decision_id_from_parts_roundtrip() {
840 let ts = 1_700_000_000_000_u64;
841 let random = 0x0012_3456_789A_BCDE_F012_u128;
842 let id = DecisionId::from_parts(ts, random);
843 assert_eq!(id.timestamp_ms(), ts);
844 assert_eq!(id.as_u128() & 0xFFFF_FFFF_FFFF_FFFF_FFFF, random);
845 }
846
847 #[test]
848 fn decision_id_from_parts_saturates_above_48_bits() {
849 let oversize = (1u64 << 48) + 12345;
850 let id = DecisionId::from_parts(oversize, 0);
851
852 assert_eq!(id.timestamp_ms(), (1u64 << 48) - 1);
853 }
854
855 #[test]
856 fn decision_id_from_parts_max_u64_saturates() {
857 let id = DecisionId::from_parts(u64::MAX, 0);
858
859 assert_eq!(id.timestamp_ms(), (1u64 << 48) - 1);
860 }
861
862 #[test]
863 fn decision_id_try_from_parts_boundary_contract() {
864 let boundary = (1u64 << 48) - 1;
865 let in_range = DecisionId::try_from_parts(boundary, 0xCAFE).expect("boundary fits");
866
867 assert_eq!(in_range.timestamp_ms(), boundary);
868 assert_eq!(
869 DecisionId::try_from_parts(1u64 << 48, 0).expect_err("out of range"),
870 1u64 << 48
871 );
872 assert_eq!(
873 DecisionId::try_from_parts(u64::MAX, 0).expect_err("out of range"),
874 u64::MAX
875 );
876 }
877
878 #[test]
879 fn decision_id_display_parse_roundtrip() {
880 let id = DecisionId::from_raw(0xDEAD_BEEF);
881 let hex = id.to_string();
882 let parsed: DecisionId = hex.parse().unwrap();
883 assert_eq!(id, parsed);
884 }
885
886 #[test]
887 fn decision_id_display_parse_roundtrip_many() {
888 for i in 0..10_000_u128 {
889 let raw = i.wrapping_mul(0xABCD_EF01_2345_6789) ^ (i << 64);
890 let id = DecisionId::from_raw(raw);
891 let hex = id.to_string();
892 let parsed: DecisionId = hex.parse().unwrap();
893 assert_eq!(id, parsed, "roundtrip failed for raw={raw:#034x}");
894 }
895 }
896
897 #[test]
898 fn decision_id_ordering() {
899 let earlier = DecisionId::from_parts(1000, 0);
900 let later = DecisionId::from_parts(2000, 0);
901 assert!(earlier < later);
902 }
903
904 #[test]
905 fn decision_id_monotonic_ordering_10k() {
906 let ids: std::vec::Vec<DecisionId> = (0..10_000)
907 .map(|i| DecisionId::from_parts(1_700_000_000_000 + i, 0))
908 .collect();
909 for window in ids.windows(2) {
910 assert!(window[0] < window[1]);
911 }
912 }
913
914 #[test]
915 fn decision_id_serde_json() {
916 let id = DecisionId::from_raw(1);
917 let json = serde_json::to_string(&id).unwrap();
918 let parsed: DecisionId = serde_json::from_str(&json).unwrap();
919 assert_eq!(id, parsed);
920 }
921
922 #[test]
923 fn decision_id_debug_format() {
924 let id = DecisionId::from_raw(0xCD);
925 let dbg = std::format!("{id:?}");
926 assert!(dbg.starts_with("DecisionId("));
927 assert!(dbg.contains("cd"));
928 }
929
930 #[test]
931 fn decision_id_copy_semantics() {
932 let id = DecisionId::from_raw(99);
933 let copy = id;
934 assert_eq!(id, copy);
935 }
936
937 #[test]
938 fn decision_id_hash_consistency() {
939 let a = DecisionId::from_raw(0xBEEF);
940 let b = DecisionId::from_raw(0xBEEF);
941 assert_eq!(a, b);
942 assert_eq!(hash_of(&a), hash_of(&b));
943 }
944
945 #[test]
946 fn decision_id_bytes_roundtrip() {
947 let id = DecisionId::from_raw(0x1234_5678_9ABC_DEF0);
948 let bytes = id.to_bytes();
949 let recovered = DecisionId::from_bytes(bytes);
950 assert_eq!(id, recovered);
951 }
952
953 #[test]
958 fn policy_id_display() {
959 let policy = PolicyId::new("scheduler.preempt", 3);
960 assert_eq!(policy.to_string(), "scheduler.preempt@v3");
961 assert_eq!(policy.name(), "scheduler.preempt");
962 assert_eq!(policy.version(), 3);
963 }
964
965 #[test]
966 fn policy_id_serde_json() {
967 let policy = PolicyId::new("cancel.budget", 1);
968 let json = serde_json::to_string(&policy).unwrap();
969 assert!(json.contains("\"n\":"));
970 assert!(json.contains("\"v\":"));
971 let parsed: PolicyId = serde_json::from_str(&json).unwrap();
972 assert_eq!(policy, parsed);
973 }
974
975 #[test]
976 fn policy_id_ordering() {
977 let a = PolicyId::new("a.policy", 1);
978 let b = PolicyId::new("b.policy", 1);
979 assert!(a < b, "PolicyId should order lexicographically by name");
980 let v1 = PolicyId::new("same", 1);
981 let v2 = PolicyId::new("same", 2);
982 assert!(v1 < v2, "same name, should order by version");
983 }
984
985 #[test]
986 fn policy_id_hash_consistency() {
987 let a = PolicyId::new("test.policy", 5);
988 let b = PolicyId::new("test.policy", 5);
989 assert_eq!(a, b);
990 assert_eq!(hash_of(&a), hash_of(&b));
991 }
992
993 #[test]
998 fn schema_version_compatible() {
999 let v1_2_3 = SchemaVersion::new(1, 2, 3);
1000 let v1_5_0 = SchemaVersion::new(1, 5, 0);
1001 let v2_0_0 = SchemaVersion::new(2, 0, 0);
1002 assert!(v1_2_3.is_compatible(&v1_5_0));
1003 assert!(!v1_2_3.is_compatible(&v2_0_0));
1004 }
1005
1006 #[test]
1007 fn schema_version_0x_edge_cases() {
1008 let v0_1 = SchemaVersion::new(0, 1, 0);
1011 let v0_2 = SchemaVersion::new(0, 2, 0);
1012 assert!(
1013 v0_1.is_compatible(&v0_2),
1014 "0.x versions should be compatible (same major=0)"
1015 );
1016
1017 let v1_0 = SchemaVersion::new(1, 0, 0);
1019 assert!(!v0_1.is_compatible(&v1_0));
1020 }
1021
1022 #[test]
1023 fn schema_version_display_parse_roundtrip() {
1024 let v = SchemaVersion::new(1, 2, 3);
1025 assert_eq!(v.to_string(), "1.2.3");
1026 let parsed: SchemaVersion = "1.2.3".parse().unwrap();
1027 assert_eq!(v, parsed);
1028 }
1029
1030 #[test]
1031 fn schema_version_ordering_comprehensive() {
1032 let versions = [
1033 SchemaVersion::new(1, 0, 0),
1034 SchemaVersion::new(1, 0, 1),
1035 SchemaVersion::new(1, 1, 0),
1036 SchemaVersion::new(2, 0, 0),
1037 SchemaVersion::new(2, 1, 0),
1038 SchemaVersion::new(10, 0, 0),
1039 ];
1040 for window in versions.windows(2) {
1041 assert!(
1042 window[0] < window[1],
1043 "{} should be < {}",
1044 window[0],
1045 window[1]
1046 );
1047 }
1048 }
1049
1050 #[test]
1051 fn schema_version_ordering() {
1052 let v1 = SchemaVersion::new(1, 0, 0);
1053 let v2 = SchemaVersion::new(2, 0, 0);
1054 assert!(v1 < v2);
1055 }
1056
1057 #[test]
1058 fn schema_version_serde_json() {
1059 let v = SchemaVersion::new(3, 1, 4);
1060 let json = serde_json::to_string(&v).unwrap();
1061 let parsed: SchemaVersion = serde_json::from_str(&json).unwrap();
1062 assert_eq!(v, parsed);
1063 }
1064
1065 #[test]
1066 fn schema_version_copy_semantics() {
1067 let v = SchemaVersion::new(1, 0, 0);
1068 let copy = v;
1069 assert_eq!(v, copy);
1070 }
1071
1072 #[test]
1073 fn schema_version_hash_consistency() {
1074 let a = SchemaVersion::new(1, 2, 3);
1075 let b = SchemaVersion::new(1, 2, 3);
1076 assert_eq!(a, b);
1077 assert_eq!(hash_of(&a), hash_of(&b));
1078 }
1079
1080 #[test]
1081 fn schema_version_self_compatible() {
1082 let v = SchemaVersion::new(5, 3, 1);
1083 assert!(
1084 v.is_compatible(&v),
1085 "version must be compatible with itself"
1086 );
1087 }
1088
1089 #[test]
1094 fn parse_id_error_display() {
1095 let err = ParseIdError {
1096 kind: "TraceId",
1097 input_len: 5,
1098 };
1099 let msg = err.to_string();
1100 assert!(msg.contains("TraceId"));
1101 assert!(msg.contains('5'));
1102 }
1103
1104 #[test]
1105 fn parse_version_error_display() {
1106 let err = ParseVersionError;
1107 let msg = err.to_string();
1108 assert!(msg.contains("major.minor.patch"));
1109 }
1110
1111 #[test]
1112 fn invalid_hex_parse_fails() {
1113 assert!("not-hex".parse::<TraceId>().is_err());
1114 assert!("not-hex".parse::<DecisionId>().is_err());
1115 }
1116
1117 #[test]
1118 fn invalid_version_parse_fails() {
1119 assert!("1.2".parse::<SchemaVersion>().is_err());
1120 assert!("a.b.c".parse::<SchemaVersion>().is_err());
1121 assert!("1.2.3.4".parse::<SchemaVersion>().is_err());
1122 assert!("".parse::<SchemaVersion>().is_err());
1123 }
1124
1125 #[test]
1130 fn all_types_send_sync() {
1131 fn assert_send_sync<T: Send + Sync>() {}
1132 assert_send_sync::<TraceId>();
1133 assert_send_sync::<DecisionId>();
1134 assert_send_sync::<PolicyId>();
1135 assert_send_sync::<SchemaVersion>();
1136 assert_send_sync::<Budget>();
1137 assert_send_sync::<NoCaps>();
1138 assert_send_sync::<Cx<'_, NoCaps>>();
1140 }
1141
1142 #[test]
1147 fn budget_new_and_remaining() {
1148 let b = Budget::new(5000);
1149 assert_eq!(b.remaining_ms(), 5000);
1150 assert!(!b.is_exhausted());
1151 }
1152
1153 #[test]
1154 fn budget_consume() {
1155 let b = Budget::new(1000);
1156 let b2 = b.consume(300).unwrap();
1157 assert_eq!(b2.remaining_ms(), 700);
1158 let b3 = b2.consume(700).unwrap();
1159 assert_eq!(b3.remaining_ms(), 0);
1160 assert!(b3.is_exhausted());
1161 }
1162
1163 #[test]
1164 fn budget_consume_insufficient() {
1165 let b = Budget::new(100);
1166 assert!(b.consume(200).is_none());
1167 }
1168
1169 #[test]
1170 fn budget_consume_exact() {
1171 let b = Budget::new(100);
1172 let b2 = b.consume(100).unwrap();
1173 assert!(b2.is_exhausted());
1174 }
1175
1176 #[test]
1177 fn budget_consume_zero() {
1178 let b = Budget::new(100);
1179 let b2 = b.consume(0).unwrap();
1180 assert_eq!(b2.remaining_ms(), 100);
1181 }
1182
1183 #[test]
1184 fn budget_min() {
1185 let b1 = Budget::new(500);
1186 let b2 = Budget::new(300);
1187 assert_eq!(b1.min(b2).remaining_ms(), 300);
1188 assert_eq!(b2.min(b1).remaining_ms(), 300);
1189 }
1190
1191 #[test]
1192 fn budget_min_equal() {
1193 let b = Budget::new(100);
1194 assert_eq!(b.min(b).remaining_ms(), 100);
1195 }
1196
1197 #[test]
1198 fn budget_unlimited() {
1199 let b = Budget::UNLIMITED;
1200 assert_eq!(b.remaining_ms(), u64::MAX);
1201 assert!(!b.is_exhausted());
1202 }
1203
1204 #[test]
1205 fn budget_unlimited_min_with_finite() {
1206 let finite = Budget::new(1000);
1207 assert_eq!(Budget::UNLIMITED.min(finite).remaining_ms(), 1000);
1208 assert_eq!(finite.min(Budget::UNLIMITED).remaining_ms(), 1000);
1209 }
1210
1211 #[test]
1212 fn budget_serde_json() {
1213 let b = Budget::new(42);
1214 let json = serde_json::to_string(&b).unwrap();
1215 let parsed: Budget = serde_json::from_str(&json).unwrap();
1216 assert_eq!(b, parsed);
1217 }
1218
1219 #[test]
1220 fn budget_copy_semantics() {
1221 let b = Budget::new(100);
1222 let copy = b;
1223 assert_eq!(b, copy);
1224 }
1225
1226 #[test]
1227 fn budget_tropical_identity() {
1228 let b = Budget::new(42);
1230 assert_eq!(b.min(Budget::UNLIMITED), b);
1231 assert_eq!(Budget::UNLIMITED.min(b), b);
1232 }
1233
1234 #[test]
1235 fn budget_tropical_commutativity() {
1236 let a = Budget::new(100);
1237 let b = Budget::new(200);
1238 assert_eq!(a.min(b), b.min(a));
1239 }
1240
1241 #[test]
1242 fn budget_tropical_associativity() {
1243 let a = Budget::new(100);
1244 let b = Budget::new(200);
1245 let c = Budget::new(50);
1246 assert_eq!(a.min(b).min(c), a.min(b.min(c)));
1247 }
1248
1249 #[test]
1254 fn no_caps_empty() {
1255 let caps = NoCaps;
1256 assert_eq!(caps.count(), 0);
1257 assert!(caps.is_empty());
1258 assert!(caps.capability_names().is_empty());
1259 }
1260
1261 #[test]
1262 fn no_caps_clone() {
1263 let a = NoCaps;
1264 let b = a.clone();
1265 assert_eq!(a, b);
1266 }
1267
1268 #[derive(Clone, Debug)]
1273 struct TestCaps {
1274 can_read: bool,
1275 can_write: bool,
1276 }
1277
1278 impl CapabilitySet for TestCaps {
1279 fn capability_names(&self) -> alloc::vec::Vec<&str> {
1280 let mut names = alloc::vec::Vec::new();
1281 if self.can_read {
1282 names.push("read");
1283 }
1284 if self.can_write {
1285 names.push("write");
1286 }
1287 names
1288 }
1289
1290 fn count(&self) -> usize {
1291 usize::from(self.can_read) + usize::from(self.can_write)
1292 }
1293 }
1294
1295 #[derive(Clone, Debug)]
1297 struct LayeredCaps {
1298 level: u32,
1299 }
1300
1301 impl CapabilitySet for LayeredCaps {
1302 fn capability_names(&self) -> alloc::vec::Vec<&str> {
1303 if self.level > 0 {
1304 alloc::vec!["layer"]
1305 } else {
1306 alloc::vec::Vec::new()
1307 }
1308 }
1309
1310 fn count(&self) -> usize {
1311 usize::from(self.level > 0)
1312 }
1313 }
1314
1315 #[test]
1320 fn cx_root_creation() {
1321 let trace = TraceId::from_parts(1_700_000_000_000, 1);
1322 let cx = Cx::new(trace, Budget::new(5000), NoCaps);
1323 assert_eq!(cx.trace_id(), trace);
1324 assert_eq!(cx.budget().remaining_ms(), 5000);
1325 assert_eq!(cx.depth(), 0);
1326 assert!(cx.capabilities().is_empty());
1327 }
1328
1329 #[test]
1330 fn cx_child_inherits_trace() {
1331 let trace = TraceId::from_parts(1_700_000_000_000, 42);
1332 let cx = Cx::new(trace, Budget::new(5000), NoCaps);
1333 let child = cx.child(NoCaps, Budget::new(3000));
1334 assert_eq!(child.trace_id(), trace);
1335 }
1336
1337 #[test]
1338 fn cx_child_budget_takes_min() {
1339 let cx = Cx::new(TraceId::from_raw(1), Budget::new(2000), NoCaps);
1340 let child1 = cx.child(NoCaps, Budget::new(1000));
1341 assert_eq!(child1.budget().remaining_ms(), 1000);
1342 let child2 = cx.child(NoCaps, Budget::new(5000));
1343 assert_eq!(child2.budget().remaining_ms(), 2000);
1344 }
1345
1346 #[test]
1347 fn cx_child_increments_depth() {
1348 let cx = Cx::new(TraceId::from_raw(1), Budget::new(1000), NoCaps);
1349 let child = cx.child(NoCaps, Budget::new(1000));
1350 assert_eq!(child.depth(), 1);
1351 let grandchild = child.child(NoCaps, Budget::new(1000));
1352 assert_eq!(grandchild.depth(), 2);
1353 }
1354
1355 #[test]
1356 fn cx_consume_budget() {
1357 let mut cx = Cx::new(TraceId::from_raw(1), Budget::new(500), NoCaps);
1358 assert!(cx.consume_budget(200));
1359 assert_eq!(cx.budget().remaining_ms(), 300);
1360 assert!(!cx.consume_budget(400));
1361 assert_eq!(cx.budget().remaining_ms(), 300);
1362 }
1363
1364 #[test]
1365 fn cx_debug_format() {
1366 let cx = Cx::new(TraceId::from_raw(0xAB), Budget::new(100), NoCaps);
1367 let dbg = std::format!("{cx:?}");
1368 assert!(dbg.contains("Cx"));
1369 assert!(dbg.contains("budget_ms"));
1370 assert!(dbg.contains("100"));
1371 }
1372
1373 #[test]
1374 fn cx_with_custom_capabilities() {
1375 let caps = TestCaps {
1376 can_read: true,
1377 can_write: false,
1378 };
1379 let cx = Cx::new(TraceId::from_raw(1), Budget::new(1000), caps);
1380 assert_eq!(cx.capabilities().count(), 1);
1381 assert_eq!(cx.capabilities().capability_names(), &["read"]);
1382 }
1383
1384 #[test]
1385 fn cx_child_with_attenuated_capabilities() {
1386 let full_caps = TestCaps {
1387 can_read: true,
1388 can_write: true,
1389 };
1390 let cx = Cx::new(TraceId::from_raw(1), Budget::new(1000), full_caps);
1391 assert_eq!(cx.capabilities().count(), 2);
1392
1393 let read_only = TestCaps {
1394 can_read: true,
1395 can_write: false,
1396 };
1397 let child = cx.child(read_only, Budget::new(500));
1398 assert_eq!(child.capabilities().count(), 1);
1399 assert!(!child.capabilities().capability_names().contains(&"write"));
1400 }
1401
1402 #[test]
1403 fn cx_capability_attenuation_chain_10x() {
1404 let trace = TraceId::from_raw(0x42);
1406 let root = Cx::new(trace, Budget::new(10_000), LayeredCaps { level: 10 });
1407 assert_eq!(root.capabilities().level, 10);
1408
1409 let mut prev_level = 10_u32;
1410 let child1 = root.child(LayeredCaps { level: 9 }, Budget::new(9000));
1411 assert!(child1.capabilities().level < prev_level);
1412 prev_level = child1.capabilities().level;
1413
1414 let child2 = child1.child(LayeredCaps { level: 8 }, Budget::new(8000));
1415 assert!(child2.capabilities().level < prev_level);
1416 prev_level = child2.capabilities().level;
1417
1418 let child3 = child2.child(LayeredCaps { level: 7 }, Budget::new(7000));
1419 assert!(child3.capabilities().level < prev_level);
1420 prev_level = child3.capabilities().level;
1421
1422 let child4 = child3.child(LayeredCaps { level: 6 }, Budget::new(6000));
1423 assert!(child4.capabilities().level < prev_level);
1424 prev_level = child4.capabilities().level;
1425
1426 let child5 = child4.child(LayeredCaps { level: 5 }, Budget::new(5000));
1427 assert!(child5.capabilities().level < prev_level);
1428 prev_level = child5.capabilities().level;
1429
1430 let child6 = child5.child(LayeredCaps { level: 4 }, Budget::new(4000));
1431 assert!(child6.capabilities().level < prev_level);
1432 prev_level = child6.capabilities().level;
1433
1434 let child7 = child6.child(LayeredCaps { level: 3 }, Budget::new(3000));
1435 assert!(child7.capabilities().level < prev_level);
1436 prev_level = child7.capabilities().level;
1437
1438 let child8 = child7.child(LayeredCaps { level: 2 }, Budget::new(2000));
1439 assert!(child8.capabilities().level < prev_level);
1440 prev_level = child8.capabilities().level;
1441
1442 let child9 = child8.child(LayeredCaps { level: 1 }, Budget::new(1000));
1443 assert!(child9.capabilities().level < prev_level);
1444 prev_level = child9.capabilities().level;
1445
1446 let child10 = child9.child(LayeredCaps { level: 0 }, Budget::new(500));
1447 assert!(child10.capabilities().level < prev_level);
1448 assert_eq!(child10.capabilities().level, 0);
1449 assert!(child10.capabilities().is_empty());
1450 assert_eq!(child10.depth(), 10);
1451
1452 assert_eq!(child10.trace_id(), trace);
1454 assert_eq!(child10.budget().remaining_ms(), 500);
1456 }
1457
1458 #[test]
1459 fn cx_deep_nesting_budget_monotonic() {
1460 let cx = Cx::new(TraceId::from_raw(1), Budget::new(1000), NoCaps);
1462 let c1 = cx.child(NoCaps, Budget::new(900));
1463 let c2 = c1.child(NoCaps, Budget::new(800));
1464 let c3 = c2.child(NoCaps, Budget::new(700));
1465 let c4 = c3.child(NoCaps, Budget::new(600));
1466
1467 assert!(c1.budget().remaining_ms() <= cx.budget().remaining_ms());
1468 assert!(c2.budget().remaining_ms() <= c1.budget().remaining_ms());
1469 assert!(c3.budget().remaining_ms() <= c2.budget().remaining_ms());
1470 assert!(c4.budget().remaining_ms() <= c3.budget().remaining_ms());
1471 }
1472
1473 #[test]
1474 fn cx_child_cannot_exceed_parent_budget() {
1475 let cx = Cx::new(TraceId::from_raw(1), Budget::new(100), NoCaps);
1476 let child = cx.child(NoCaps, Budget::UNLIMITED);
1478 assert_eq!(child.budget().remaining_ms(), 100);
1479 }
1480
1481 #[test]
1482 fn cx_trace_propagation_through_chain() {
1483 let trace = TraceId::from_parts(1_700_000_000_000, 0xCAFE);
1484 let cx = Cx::new(trace, Budget::UNLIMITED, NoCaps);
1485 let c1 = cx.child(NoCaps, Budget::UNLIMITED);
1486 let c2 = c1.child(NoCaps, Budget::UNLIMITED);
1487 let c3 = c2.child(NoCaps, Budget::UNLIMITED);
1488 assert_eq!(c3.trace_id(), trace);
1489 assert_eq!(c3.depth(), 3);
1490 }
1491
1492 #[test]
1497 fn _97gwup_from_parts_within_range_round_trips() {
1498 let ts = 1_700_000_000_000;
1500 let trace = TraceId::from_parts(ts, 0xDEAD_BEEF);
1501 assert_eq!(trace.timestamp_ms(), ts);
1502 }
1503
1504 #[test]
1505 fn _97gwup_from_parts_at_48_bit_boundary() {
1506 let ts = (1u64 << 48) - 1;
1508 let trace = TraceId::from_parts(ts, 0);
1509 assert_eq!(trace.timestamp_ms(), ts);
1510 }
1511
1512 #[test]
1513 fn _97gwup_from_parts_saturates_above_48_bits() {
1514 let oversize = (1u64 << 48) + 12345;
1518 let trace = TraceId::from_parts(oversize, 0);
1519 assert_eq!(trace.timestamp_ms(), (1u64 << 48) - 1);
1520 }
1521
1522 #[test]
1523 fn _97gwup_from_parts_max_u64_saturates() {
1524 let trace = TraceId::from_parts(u64::MAX, 0);
1525 assert_eq!(trace.timestamp_ms(), (1u64 << 48) - 1);
1526 }
1527
1528 #[test]
1529 fn _97gwup_try_from_parts_within_range_ok() {
1530 let ts = 1_700_000_000_000;
1531 let trace = TraceId::try_from_parts(ts, 0xCAFE).expect("in range");
1532 assert_eq!(trace.timestamp_ms(), ts);
1533 }
1534
1535 #[test]
1536 fn _97gwup_try_from_parts_at_boundary_ok() {
1537 let ts = (1u64 << 48) - 1;
1538 assert!(TraceId::try_from_parts(ts, 0).is_ok());
1539 }
1540
1541 #[test]
1542 fn _97gwup_try_from_parts_above_boundary_err() {
1543 let ts = 1u64 << 48;
1544 let err = TraceId::try_from_parts(ts, 0).expect_err("out of range");
1545 assert_eq!(err, ts);
1546 }
1547
1548 #[test]
1549 fn _97gwup_try_from_parts_max_u64_err() {
1550 let err = TraceId::try_from_parts(u64::MAX, 0).expect_err("out of range");
1551 assert_eq!(err, u64::MAX);
1552 }
1553}
1554
1555#[cfg(test)]
1560mod proptest_tests {
1561 extern crate std;
1562
1563 use super::*;
1564 use core::hash::{Hash, Hasher};
1565 use proptest::prelude::*;
1566 use std::collections::hash_map::DefaultHasher;
1567 use std::string::ToString;
1568
1569 fn hash_of<T: Hash>(val: &T) -> u64 {
1570 let mut h = DefaultHasher::new();
1571 val.hash(&mut h);
1572 h.finish()
1573 }
1574
1575 proptest! {
1578 #[test]
1579 fn trace_id_display_fromstr_roundtrip(raw: u128) {
1580 let id = TraceId::from_raw(raw);
1581 let hex = id.to_string();
1582 let parsed: TraceId = hex.parse().unwrap();
1583 prop_assert_eq!(id, parsed);
1584 }
1585
1586 #[test]
1587 fn trace_id_serde_roundtrip(raw: u128) {
1588 let id = TraceId::from_raw(raw);
1589 let json = serde_json::to_string(&id).unwrap();
1590 let parsed: TraceId = serde_json::from_str(&json).unwrap();
1591 prop_assert_eq!(id, parsed);
1592 }
1593
1594 #[test]
1595 fn trace_id_bytes_roundtrip(raw: u128) {
1596 let id = TraceId::from_raw(raw);
1597 let bytes = id.to_bytes();
1598 let recovered = TraceId::from_bytes(bytes);
1599 prop_assert_eq!(id, recovered);
1600 }
1601
1602 #[test]
1603 fn trace_id_hash_consistency(a: u128, b: u128) {
1604 let id_a = TraceId::from_raw(a);
1605 let id_b = TraceId::from_raw(b);
1606 if id_a == id_b {
1607 prop_assert_eq!(hash_of(&id_a), hash_of(&id_b));
1608 }
1609 }
1610
1611 #[test]
1612 fn trace_id_from_parts_preserves_timestamp(ts_ms: u64, random: u128) {
1613 let ts_masked = ts_ms & 0xFFFF_FFFF_FFFF;
1615 let id = TraceId::from_parts(ts_masked, random);
1616 prop_assert_eq!(id.timestamp_ms(), ts_masked);
1617 }
1618 }
1619
1620 proptest! {
1623 #[test]
1624 fn decision_id_display_fromstr_roundtrip(raw: u128) {
1625 let id = DecisionId::from_raw(raw);
1626 let hex = id.to_string();
1627 let parsed: DecisionId = hex.parse().unwrap();
1628 prop_assert_eq!(id, parsed);
1629 }
1630
1631 #[test]
1632 fn decision_id_serde_roundtrip(raw: u128) {
1633 let id = DecisionId::from_raw(raw);
1634 let json = serde_json::to_string(&id).unwrap();
1635 let parsed: DecisionId = serde_json::from_str(&json).unwrap();
1636 prop_assert_eq!(id, parsed);
1637 }
1638
1639 #[test]
1640 fn decision_id_hash_consistency(a: u128, b: u128) {
1641 let id_a = DecisionId::from_raw(a);
1642 let id_b = DecisionId::from_raw(b);
1643 if id_a == id_b {
1644 prop_assert_eq!(hash_of(&id_a), hash_of(&id_b));
1645 }
1646 }
1647 }
1648
1649 proptest! {
1652 #[test]
1653 fn schema_version_parse_roundtrip(major: u32, minor: u32, patch: u32) {
1654 let v = SchemaVersion::new(major, minor, patch);
1655 let s = v.to_string();
1656 let parsed: SchemaVersion = s.parse().unwrap();
1657 prop_assert_eq!(v, parsed);
1658 }
1659
1660 #[test]
1661 fn schema_version_serde_roundtrip(major: u32, minor: u32, patch: u32) {
1662 let v = SchemaVersion::new(major, minor, patch);
1663 let json = serde_json::to_string(&v).unwrap();
1664 let parsed: SchemaVersion = serde_json::from_str(&json).unwrap();
1665 prop_assert_eq!(v, parsed);
1666 }
1667
1668 #[test]
1669 fn schema_version_compatible_reflexive(major: u32, minor: u32, patch: u32) {
1670 let v = SchemaVersion::new(major, minor, patch);
1671 prop_assert!(v.is_compatible(&v));
1672 }
1673
1674 #[test]
1675 fn schema_version_compatible_symmetric(
1676 m1: u32, n1: u32, p1: u32,
1677 m2: u32, n2: u32, p2: u32
1678 ) {
1679 let a = SchemaVersion::new(m1, n1, p1);
1680 let b = SchemaVersion::new(m2, n2, p2);
1681 prop_assert_eq!(a.is_compatible(&b), b.is_compatible(&a));
1682 }
1683
1684 #[test]
1685 fn schema_version_compatible_transitive(
1686 m1: u32, n1: u32, p1: u32,
1687 n2: u32, p2: u32,
1688 n3: u32, p3: u32
1689 ) {
1690 let a = SchemaVersion::new(m1, n1, p1);
1693 let b = SchemaVersion::new(m1, n2, p2);
1694 let c = SchemaVersion::new(m1, n3, p3);
1695 if a.is_compatible(&b) && b.is_compatible(&c) {
1696 prop_assert!(a.is_compatible(&c));
1697 }
1698 }
1699
1700 #[test]
1701 fn schema_version_hash_consistency(
1702 m1: u32, n1: u32, p1: u32,
1703 m2: u32, n2: u32, p2: u32
1704 ) {
1705 let a = SchemaVersion::new(m1, n1, p1);
1706 let b = SchemaVersion::new(m2, n2, p2);
1707 if a == b {
1708 prop_assert_eq!(hash_of(&a), hash_of(&b));
1709 }
1710 }
1711 }
1712
1713 proptest! {
1716 #[test]
1717 fn budget_min_commutative(a: u64, b: u64) {
1718 let ba = Budget::new(a);
1719 let bb = Budget::new(b);
1720 prop_assert_eq!(ba.min(bb), bb.min(ba));
1721 }
1722
1723 #[test]
1724 fn budget_min_associative(a: u64, b: u64, c: u64) {
1725 let ba = Budget::new(a);
1726 let bb = Budget::new(b);
1727 let bc = Budget::new(c);
1728 prop_assert_eq!(ba.min(bb).min(bc), ba.min(bb.min(bc)));
1729 }
1730
1731 #[test]
1732 fn budget_min_identity(a: u64) {
1733 let ba = Budget::new(a);
1735 prop_assert_eq!(ba.min(Budget::UNLIMITED), ba);
1736 prop_assert_eq!(Budget::UNLIMITED.min(ba), ba);
1737 }
1738
1739 #[test]
1740 fn budget_min_idempotent(a: u64) {
1741 let ba = Budget::new(a);
1742 prop_assert_eq!(ba.min(ba), ba);
1743 }
1744
1745 #[test]
1746 fn budget_consume_additive(total in 0..=10_000_u64, a in 0..=5_000_u64, b in 0..=5_000_u64) {
1747 let budget = Budget::new(total);
1749 if a + b <= total {
1750 let after_both = budget.consume(a + b).unwrap();
1751 let after_a = budget.consume(a).unwrap();
1752 let after_ab = after_a.consume(b).unwrap();
1753 prop_assert_eq!(after_both.remaining_ms(), after_ab.remaining_ms());
1754 }
1755 }
1756
1757 #[test]
1758 fn budget_serde_roundtrip(ms: u64) {
1759 let b = Budget::new(ms);
1760 let json = serde_json::to_string(&b).unwrap();
1761 let parsed: Budget = serde_json::from_str(&json).unwrap();
1762 prop_assert_eq!(b, parsed);
1763 }
1764
1765 #[test]
1766 fn budget_hash_consistency(a: u64, b: u64) {
1767 let ba = Budget::new(a);
1768 let bb = Budget::new(b);
1769 if ba == bb {
1770 prop_assert_eq!(hash_of(&ba), hash_of(&bb));
1771 }
1772 }
1773 }
1774
1775 proptest! {
1778 #[test]
1779 fn cx_child_budget_never_exceeds_parent(parent_ms: u64, child_ms: u64) {
1780 let trace = TraceId::from_raw(1);
1781 let cx = Cx::new(trace, Budget::new(parent_ms), NoCaps);
1782 let child = cx.child(NoCaps, Budget::new(child_ms));
1783 prop_assert!(child.budget().remaining_ms() <= cx.budget().remaining_ms());
1784 }
1785
1786 #[test]
1787 fn cx_child_trace_always_inherited(raw: u128, budget_ms: u64) {
1788 let trace = TraceId::from_raw(raw);
1789 let cx = Cx::new(trace, Budget::new(budget_ms), NoCaps);
1790 let child = cx.child(NoCaps, Budget::new(budget_ms));
1791 prop_assert_eq!(child.trace_id(), trace);
1792 }
1793
1794 #[test]
1795 fn cx_child_depth_increments(raw: u128, budget_ms: u64) {
1796 let cx = Cx::new(TraceId::from_raw(raw), Budget::new(budget_ms), NoCaps);
1797 let child = cx.child(NoCaps, Budget::new(budget_ms));
1798 prop_assert_eq!(child.depth(), cx.depth() + 1);
1799 }
1800 }
1801}