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]
197 pub const fn from_parts(ts_ms: u64, random: u128) -> Self {
198 let ts_bits = (ts_ms as u128) << 80;
199 let rand_bits = random & 0xFFFF_FFFF_FFFF_FFFF_FFFF;
200 Self(ts_bits | rand_bits)
201 }
202
203 #[must_use]
205 pub const fn timestamp_ms(self) -> u64 {
206 (self.0 >> 80) as u64
207 }
208
209 #[must_use]
211 pub const fn as_u128(self) -> u128 {
212 self.0
213 }
214
215 #[must_use]
217 pub const fn to_bytes(self) -> [u8; 16] {
218 self.0.to_be_bytes()
219 }
220
221 pub const fn from_bytes(bytes: [u8; 16]) -> Self {
223 Self(u128::from_be_bytes(bytes))
224 }
225}
226
227impl fmt::Debug for DecisionId {
228 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229 write!(f, "DecisionId({:032x})", self.0)
230 }
231}
232
233impl fmt::Display for DecisionId {
234 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
235 write!(f, "{:032x}", self.0)
236 }
237}
238
239impl FromStr for DecisionId {
240 type Err = ParseIdError;
241
242 fn from_str(s: &str) -> Result<Self, Self::Err> {
243 let val = u128::from_str_radix(s, 16).map_err(|_| ParseIdError {
244 kind: "DecisionId",
245 input_len: s.len(),
246 })?;
247 Ok(Self(val))
248 }
249}
250
251#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
267pub struct PolicyId {
268 #[serde(rename = "n")]
270 name: String,
271 #[serde(rename = "v")]
273 version: u32,
274}
275
276impl PolicyId {
277 pub fn new(name: impl Into<String>, version: u32) -> Self {
279 Self {
280 name: name.into(),
281 version,
282 }
283 }
284
285 pub fn name(&self) -> &str {
287 &self.name
288 }
289
290 pub const fn version(&self) -> u32 {
292 self.version
293 }
294}
295
296impl fmt::Display for PolicyId {
297 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
298 write!(f, "{}@v{}", self.name, self.version)
299 }
300}
301
302#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
321pub struct SchemaVersion {
322 pub major: u32,
324 pub minor: u32,
326 pub patch: u32,
328}
329
330impl SchemaVersion {
331 pub const fn new(major: u32, minor: u32, patch: u32) -> Self {
333 Self {
334 major,
335 minor,
336 patch,
337 }
338 }
339
340 pub const fn is_compatible(&self, other: &Self) -> bool {
342 self.major == other.major
343 }
344}
345
346impl fmt::Display for SchemaVersion {
347 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
348 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
349 }
350}
351
352impl FromStr for SchemaVersion {
353 type Err = ParseVersionError;
354
355 fn from_str(s: &str) -> Result<Self, Self::Err> {
356 let parts: alloc::vec::Vec<&str> = s.split('.').collect();
357 if parts.len() != 3 {
358 return Err(ParseVersionError);
359 }
360 let major = parts[0].parse().map_err(|_| ParseVersionError)?;
361 let minor = parts[1].parse().map_err(|_| ParseVersionError)?;
362 let patch = parts[2].parse().map_err(|_| ParseVersionError)?;
363 Ok(Self {
364 major,
365 minor,
366 patch,
367 })
368 }
369}
370
371#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
389pub struct Budget {
390 remaining_ms: u64,
391}
392
393impl Budget {
394 pub const fn new(ms: u64) -> Self {
396 Self { remaining_ms: ms }
397 }
398
399 pub const fn remaining_ms(self) -> u64 {
401 self.remaining_ms
402 }
403
404 pub const fn consume(self, ms: u64) -> Option<Self> {
408 if self.remaining_ms >= ms {
409 Some(Self {
410 remaining_ms: self.remaining_ms - ms,
411 })
412 } else {
413 None
414 }
415 }
416
417 pub const fn is_exhausted(self) -> bool {
419 self.remaining_ms == 0
420 }
421
422 #[must_use]
424 pub const fn min(self, other: Self) -> Self {
425 if self.remaining_ms <= other.remaining_ms {
426 self
427 } else {
428 other
429 }
430 }
431
432 pub const UNLIMITED: Self = Self {
434 remaining_ms: u64::MAX,
435 };
436}
437
438pub trait CapabilitySet: Clone + fmt::Debug + Send + Sync {
451 fn capability_names(&self) -> Vec<&str>;
453
454 fn count(&self) -> usize;
456
457 fn is_empty(&self) -> bool {
459 self.count() == 0
460 }
461}
462
463#[derive(Clone, Debug, Default, PartialEq, Eq)]
465pub struct NoCaps;
466
467impl CapabilitySet for NoCaps {
468 fn capability_names(&self) -> Vec<&str> {
469 Vec::new()
470 }
471
472 fn count(&self) -> usize {
473 0
474 }
475}
476
477pub struct Cx<'a, C: CapabilitySet = NoCaps> {
500 trace_id: TraceId,
501 budget: Budget,
502 capabilities: C,
503 depth: u32,
504 _scope: PhantomData<&'a ()>,
505}
506
507impl<C: CapabilitySet> Cx<'_, C> {
508 pub fn new(trace_id: TraceId, budget: Budget, capabilities: C) -> Self {
510 Self {
511 trace_id,
512 budget,
513 capabilities,
514 depth: 0,
515 _scope: PhantomData,
516 }
517 }
518
519 pub fn child(&self, capabilities: C, budget: Budget) -> Cx<'_, C> {
524 Cx {
525 trace_id: self.trace_id,
526 budget: self.budget.min(budget),
527 capabilities,
528 depth: self.depth + 1,
529 _scope: PhantomData,
530 }
531 }
532
533 pub const fn trace_id(&self) -> TraceId {
535 self.trace_id
536 }
537
538 pub const fn budget(&self) -> Budget {
540 self.budget
541 }
542
543 pub fn capabilities(&self) -> &C {
545 &self.capabilities
546 }
547
548 pub const fn depth(&self) -> u32 {
550 self.depth
551 }
552
553 pub fn consume_budget(&mut self, ms: u64) -> bool {
557 match self.budget.consume(ms) {
558 Some(new_budget) => {
559 self.budget = new_budget;
560 true
561 }
562 None => false,
563 }
564 }
565}
566
567impl<C: CapabilitySet> fmt::Debug for Cx<'_, C> {
568 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
569 f.debug_struct("Cx")
570 .field("trace_id", &self.trace_id)
571 .field("budget_ms", &self.budget.remaining_ms())
572 .field("capabilities", &self.capabilities)
573 .field("depth", &self.depth)
574 .finish()
575 }
576}
577
578#[derive(Clone, Debug, PartialEq, Eq)]
584pub struct ParseIdError {
585 pub kind: &'static str,
587 pub input_len: usize,
589}
590
591impl fmt::Display for ParseIdError {
592 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
593 write!(
594 f,
595 "invalid {} hex string (length {})",
596 self.kind, self.input_len
597 )
598 }
599}
600
601#[derive(Clone, Debug, PartialEq, Eq)]
603pub struct ParseVersionError;
604
605impl fmt::Display for ParseVersionError {
606 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
607 write!(f, "invalid schema version (expected major.minor.patch)")
608 }
609}
610
611mod hex_u128 {
616 use alloc::format;
617 use alloc::string::String;
618
619 use serde::{self, Deserialize, Deserializer, Serializer};
620
621 pub fn serialize<S>(value: &u128, serializer: S) -> Result<S::Ok, S::Error>
622 where
623 S: Serializer,
624 {
625 serializer.serialize_str(&format!("{value:032x}"))
626 }
627
628 pub fn deserialize<'de, D>(deserializer: D) -> Result<u128, D::Error>
629 where
630 D: Deserializer<'de>,
631 {
632 let s = String::deserialize(deserializer)?;
633 u128::from_str_radix(&s, 16)
634 .map_err(|_| serde::de::Error::custom(format!("invalid hex u128: {s}")))
635 }
636}
637
638#[cfg(test)]
643mod tests {
644 extern crate std;
645
646 use super::*;
647 use core::hash::{Hash, Hasher};
648 use std::collections::hash_map::DefaultHasher;
649 use std::string::ToString;
650
651 fn hash_of<T: Hash>(val: &T) -> u64 {
652 let mut h = DefaultHasher::new();
653 val.hash(&mut h);
654 h.finish()
655 }
656
657 #[test]
662 fn trace_id_from_parts_roundtrip() {
663 let ts = 1_700_000_000_000_u64;
664 let random = 0x00AB_CDEF_0123_4567_89AB_u128;
665 let id = TraceId::from_parts(ts, random);
666 assert_eq!(id.timestamp_ms(), ts);
667 assert_eq!(id.as_u128() & 0xFFFF_FFFF_FFFF_FFFF_FFFF, random);
668 }
669
670 #[test]
671 fn trace_id_display_parse_roundtrip() {
672 let id = TraceId::from_raw(0x0123_4567_89AB_CDEF_0123_4567_89AB_CDEF);
673 let hex = id.to_string();
674 assert_eq!(hex, "0123456789abcdef0123456789abcdef");
675 let parsed: TraceId = hex.parse().unwrap();
676 assert_eq!(id, parsed);
677 }
678
679 #[test]
680 fn trace_id_bytes_roundtrip() {
681 let id = TraceId::from_raw(42);
682 let bytes = id.to_bytes();
683 let recovered = TraceId::from_bytes(bytes);
684 assert_eq!(id, recovered);
685 }
686
687 #[test]
688 fn trace_id_ordering() {
689 let earlier = TraceId::from_parts(1000, 0);
690 let later = TraceId::from_parts(2000, 0);
691 assert!(earlier < later);
692 }
693
694 #[test]
695 fn trace_id_uuidv7_monotonic_ordering_10k() {
696 let ids: std::vec::Vec<TraceId> = (0..10_000)
698 .map(|i| TraceId::from_parts(1_700_000_000_000 + i, 0))
699 .collect();
700 for window in ids.windows(2) {
701 assert!(
702 window[0] < window[1],
703 "TraceId ordering violated: {:?} >= {:?}",
704 window[0],
705 window[1]
706 );
707 }
708 }
709
710 #[test]
711 fn trace_id_display_parse_roundtrip_many() {
712 for i in 0..10_000_u128 {
714 let raw = i.wrapping_mul(0x0123_4567_89AB_CDEF) ^ (i << 64);
715 let id = TraceId::from_raw(raw);
716 let hex = id.to_string();
717 let parsed: TraceId = hex.parse().unwrap();
718 assert_eq!(id, parsed, "roundtrip failed for raw={raw:#034x}");
719 }
720 }
721
722 #[test]
723 fn trace_id_serde_json() {
724 let id = TraceId::from_raw(0xFF);
725 let json = serde_json::to_string(&id).unwrap();
726 assert_eq!(json, "\"000000000000000000000000000000ff\"");
727 let parsed: TraceId = serde_json::from_str(&json).unwrap();
728 assert_eq!(id, parsed);
729 }
730
731 #[test]
732 fn trace_id_serde_roundtrip_many() {
733 for i in 0..1_000_u128 {
734 let id = TraceId::from_raw(i.wrapping_mul(0xDEAD_BEEF_CAFE_1234));
735 let json = serde_json::to_string(&id).unwrap();
736 let parsed: TraceId = serde_json::from_str(&json).unwrap();
737 assert_eq!(id, parsed);
738 }
739 }
740
741 #[test]
742 fn trace_id_debug_format() {
743 let id = TraceId::from_raw(0xAB);
744 let dbg = std::format!("{id:?}");
745 assert!(dbg.starts_with("TraceId("));
746 assert!(dbg.contains("ab"));
747 }
748
749 #[test]
750 fn trace_id_copy_semantics() {
751 let id = TraceId::from_raw(42);
752 let copy = id;
753 assert_eq!(id, copy); }
755
756 #[test]
757 fn trace_id_hash_consistency() {
758 let a = TraceId::from_raw(0xDEAD);
759 let b = TraceId::from_raw(0xDEAD);
760 assert_eq!(a, b);
761 assert_eq!(hash_of(&a), hash_of(&b));
762 }
763
764 #[test]
765 fn trace_id_zero_and_max() {
766 let zero = TraceId::from_raw(0);
767 assert_eq!(zero.timestamp_ms(), 0);
768 assert_eq!(zero.to_string(), "00000000000000000000000000000000");
769 let roundtrip: TraceId = zero.to_string().parse().unwrap();
770 assert_eq!(zero, roundtrip);
771
772 let max = TraceId::from_raw(u128::MAX);
773 assert_eq!(max.to_string(), "ffffffffffffffffffffffffffffffff");
774 let roundtrip: TraceId = max.to_string().parse().unwrap();
775 assert_eq!(max, roundtrip);
776 }
777
778 #[test]
783 fn decision_id_from_parts_roundtrip() {
784 let ts = 1_700_000_000_000_u64;
785 let random = 0x0012_3456_789A_BCDE_F012_u128;
786 let id = DecisionId::from_parts(ts, random);
787 assert_eq!(id.timestamp_ms(), ts);
788 assert_eq!(id.as_u128() & 0xFFFF_FFFF_FFFF_FFFF_FFFF, random);
789 }
790
791 #[test]
792 fn decision_id_display_parse_roundtrip() {
793 let id = DecisionId::from_raw(0xDEAD_BEEF);
794 let hex = id.to_string();
795 let parsed: DecisionId = hex.parse().unwrap();
796 assert_eq!(id, parsed);
797 }
798
799 #[test]
800 fn decision_id_display_parse_roundtrip_many() {
801 for i in 0..10_000_u128 {
802 let raw = i.wrapping_mul(0xABCD_EF01_2345_6789) ^ (i << 64);
803 let id = DecisionId::from_raw(raw);
804 let hex = id.to_string();
805 let parsed: DecisionId = hex.parse().unwrap();
806 assert_eq!(id, parsed, "roundtrip failed for raw={raw:#034x}");
807 }
808 }
809
810 #[test]
811 fn decision_id_ordering() {
812 let earlier = DecisionId::from_parts(1000, 0);
813 let later = DecisionId::from_parts(2000, 0);
814 assert!(earlier < later);
815 }
816
817 #[test]
818 fn decision_id_monotonic_ordering_10k() {
819 let ids: std::vec::Vec<DecisionId> = (0..10_000)
820 .map(|i| DecisionId::from_parts(1_700_000_000_000 + i, 0))
821 .collect();
822 for window in ids.windows(2) {
823 assert!(window[0] < window[1]);
824 }
825 }
826
827 #[test]
828 fn decision_id_serde_json() {
829 let id = DecisionId::from_raw(1);
830 let json = serde_json::to_string(&id).unwrap();
831 let parsed: DecisionId = serde_json::from_str(&json).unwrap();
832 assert_eq!(id, parsed);
833 }
834
835 #[test]
836 fn decision_id_debug_format() {
837 let id = DecisionId::from_raw(0xCD);
838 let dbg = std::format!("{id:?}");
839 assert!(dbg.starts_with("DecisionId("));
840 assert!(dbg.contains("cd"));
841 }
842
843 #[test]
844 fn decision_id_copy_semantics() {
845 let id = DecisionId::from_raw(99);
846 let copy = id;
847 assert_eq!(id, copy);
848 }
849
850 #[test]
851 fn decision_id_hash_consistency() {
852 let a = DecisionId::from_raw(0xBEEF);
853 let b = DecisionId::from_raw(0xBEEF);
854 assert_eq!(a, b);
855 assert_eq!(hash_of(&a), hash_of(&b));
856 }
857
858 #[test]
859 fn decision_id_bytes_roundtrip() {
860 let id = DecisionId::from_raw(0x1234_5678_9ABC_DEF0);
861 let bytes = id.to_bytes();
862 let recovered = DecisionId::from_bytes(bytes);
863 assert_eq!(id, recovered);
864 }
865
866 #[test]
871 fn policy_id_display() {
872 let policy = PolicyId::new("scheduler.preempt", 3);
873 assert_eq!(policy.to_string(), "scheduler.preempt@v3");
874 assert_eq!(policy.name(), "scheduler.preempt");
875 assert_eq!(policy.version(), 3);
876 }
877
878 #[test]
879 fn policy_id_serde_json() {
880 let policy = PolicyId::new("cancel.budget", 1);
881 let json = serde_json::to_string(&policy).unwrap();
882 assert!(json.contains("\"n\":"));
883 assert!(json.contains("\"v\":"));
884 let parsed: PolicyId = serde_json::from_str(&json).unwrap();
885 assert_eq!(policy, parsed);
886 }
887
888 #[test]
889 fn policy_id_ordering() {
890 let a = PolicyId::new("a.policy", 1);
891 let b = PolicyId::new("b.policy", 1);
892 assert!(a < b, "PolicyId should order lexicographically by name");
893 let v1 = PolicyId::new("same", 1);
894 let v2 = PolicyId::new("same", 2);
895 assert!(v1 < v2, "same name, should order by version");
896 }
897
898 #[test]
899 fn policy_id_hash_consistency() {
900 let a = PolicyId::new("test.policy", 5);
901 let b = PolicyId::new("test.policy", 5);
902 assert_eq!(a, b);
903 assert_eq!(hash_of(&a), hash_of(&b));
904 }
905
906 #[test]
911 fn schema_version_compatible() {
912 let v1_2_3 = SchemaVersion::new(1, 2, 3);
913 let v1_5_0 = SchemaVersion::new(1, 5, 0);
914 let v2_0_0 = SchemaVersion::new(2, 0, 0);
915 assert!(v1_2_3.is_compatible(&v1_5_0));
916 assert!(!v1_2_3.is_compatible(&v2_0_0));
917 }
918
919 #[test]
920 fn schema_version_0x_edge_cases() {
921 let v0_1 = SchemaVersion::new(0, 1, 0);
924 let v0_2 = SchemaVersion::new(0, 2, 0);
925 assert!(
926 v0_1.is_compatible(&v0_2),
927 "0.x versions should be compatible (same major=0)"
928 );
929
930 let v1_0 = SchemaVersion::new(1, 0, 0);
932 assert!(!v0_1.is_compatible(&v1_0));
933 }
934
935 #[test]
936 fn schema_version_display_parse_roundtrip() {
937 let v = SchemaVersion::new(1, 2, 3);
938 assert_eq!(v.to_string(), "1.2.3");
939 let parsed: SchemaVersion = "1.2.3".parse().unwrap();
940 assert_eq!(v, parsed);
941 }
942
943 #[test]
944 fn schema_version_ordering_comprehensive() {
945 let versions = [
946 SchemaVersion::new(1, 0, 0),
947 SchemaVersion::new(1, 0, 1),
948 SchemaVersion::new(1, 1, 0),
949 SchemaVersion::new(2, 0, 0),
950 SchemaVersion::new(2, 1, 0),
951 SchemaVersion::new(10, 0, 0),
952 ];
953 for window in versions.windows(2) {
954 assert!(
955 window[0] < window[1],
956 "{} should be < {}",
957 window[0],
958 window[1]
959 );
960 }
961 }
962
963 #[test]
964 fn schema_version_ordering() {
965 let v1 = SchemaVersion::new(1, 0, 0);
966 let v2 = SchemaVersion::new(2, 0, 0);
967 assert!(v1 < v2);
968 }
969
970 #[test]
971 fn schema_version_serde_json() {
972 let v = SchemaVersion::new(3, 1, 4);
973 let json = serde_json::to_string(&v).unwrap();
974 let parsed: SchemaVersion = serde_json::from_str(&json).unwrap();
975 assert_eq!(v, parsed);
976 }
977
978 #[test]
979 fn schema_version_copy_semantics() {
980 let v = SchemaVersion::new(1, 0, 0);
981 let copy = v;
982 assert_eq!(v, copy);
983 }
984
985 #[test]
986 fn schema_version_hash_consistency() {
987 let a = SchemaVersion::new(1, 2, 3);
988 let b = SchemaVersion::new(1, 2, 3);
989 assert_eq!(a, b);
990 assert_eq!(hash_of(&a), hash_of(&b));
991 }
992
993 #[test]
994 fn schema_version_self_compatible() {
995 let v = SchemaVersion::new(5, 3, 1);
996 assert!(
997 v.is_compatible(&v),
998 "version must be compatible with itself"
999 );
1000 }
1001
1002 #[test]
1007 fn parse_id_error_display() {
1008 let err = ParseIdError {
1009 kind: "TraceId",
1010 input_len: 5,
1011 };
1012 let msg = err.to_string();
1013 assert!(msg.contains("TraceId"));
1014 assert!(msg.contains('5'));
1015 }
1016
1017 #[test]
1018 fn parse_version_error_display() {
1019 let err = ParseVersionError;
1020 let msg = err.to_string();
1021 assert!(msg.contains("major.minor.patch"));
1022 }
1023
1024 #[test]
1025 fn invalid_hex_parse_fails() {
1026 assert!("not-hex".parse::<TraceId>().is_err());
1027 assert!("not-hex".parse::<DecisionId>().is_err());
1028 }
1029
1030 #[test]
1031 fn invalid_version_parse_fails() {
1032 assert!("1.2".parse::<SchemaVersion>().is_err());
1033 assert!("a.b.c".parse::<SchemaVersion>().is_err());
1034 assert!("1.2.3.4".parse::<SchemaVersion>().is_err());
1035 assert!("".parse::<SchemaVersion>().is_err());
1036 }
1037
1038 #[test]
1043 fn all_types_send_sync() {
1044 fn assert_send_sync<T: Send + Sync>() {}
1045 assert_send_sync::<TraceId>();
1046 assert_send_sync::<DecisionId>();
1047 assert_send_sync::<PolicyId>();
1048 assert_send_sync::<SchemaVersion>();
1049 assert_send_sync::<Budget>();
1050 assert_send_sync::<NoCaps>();
1051 assert_send_sync::<Cx<'_, NoCaps>>();
1053 }
1054
1055 #[test]
1060 fn budget_new_and_remaining() {
1061 let b = Budget::new(5000);
1062 assert_eq!(b.remaining_ms(), 5000);
1063 assert!(!b.is_exhausted());
1064 }
1065
1066 #[test]
1067 fn budget_consume() {
1068 let b = Budget::new(1000);
1069 let b2 = b.consume(300).unwrap();
1070 assert_eq!(b2.remaining_ms(), 700);
1071 let b3 = b2.consume(700).unwrap();
1072 assert_eq!(b3.remaining_ms(), 0);
1073 assert!(b3.is_exhausted());
1074 }
1075
1076 #[test]
1077 fn budget_consume_insufficient() {
1078 let b = Budget::new(100);
1079 assert!(b.consume(200).is_none());
1080 }
1081
1082 #[test]
1083 fn budget_consume_exact() {
1084 let b = Budget::new(100);
1085 let b2 = b.consume(100).unwrap();
1086 assert!(b2.is_exhausted());
1087 }
1088
1089 #[test]
1090 fn budget_consume_zero() {
1091 let b = Budget::new(100);
1092 let b2 = b.consume(0).unwrap();
1093 assert_eq!(b2.remaining_ms(), 100);
1094 }
1095
1096 #[test]
1097 fn budget_min() {
1098 let b1 = Budget::new(500);
1099 let b2 = Budget::new(300);
1100 assert_eq!(b1.min(b2).remaining_ms(), 300);
1101 assert_eq!(b2.min(b1).remaining_ms(), 300);
1102 }
1103
1104 #[test]
1105 fn budget_min_equal() {
1106 let b = Budget::new(100);
1107 assert_eq!(b.min(b).remaining_ms(), 100);
1108 }
1109
1110 #[test]
1111 fn budget_unlimited() {
1112 let b = Budget::UNLIMITED;
1113 assert_eq!(b.remaining_ms(), u64::MAX);
1114 assert!(!b.is_exhausted());
1115 }
1116
1117 #[test]
1118 fn budget_unlimited_min_with_finite() {
1119 let finite = Budget::new(1000);
1120 assert_eq!(Budget::UNLIMITED.min(finite).remaining_ms(), 1000);
1121 assert_eq!(finite.min(Budget::UNLIMITED).remaining_ms(), 1000);
1122 }
1123
1124 #[test]
1125 fn budget_serde_json() {
1126 let b = Budget::new(42);
1127 let json = serde_json::to_string(&b).unwrap();
1128 let parsed: Budget = serde_json::from_str(&json).unwrap();
1129 assert_eq!(b, parsed);
1130 }
1131
1132 #[test]
1133 fn budget_copy_semantics() {
1134 let b = Budget::new(100);
1135 let copy = b;
1136 assert_eq!(b, copy);
1137 }
1138
1139 #[test]
1140 fn budget_tropical_identity() {
1141 let b = Budget::new(42);
1143 assert_eq!(b.min(Budget::UNLIMITED), b);
1144 assert_eq!(Budget::UNLIMITED.min(b), b);
1145 }
1146
1147 #[test]
1148 fn budget_tropical_commutativity() {
1149 let a = Budget::new(100);
1150 let b = Budget::new(200);
1151 assert_eq!(a.min(b), b.min(a));
1152 }
1153
1154 #[test]
1155 fn budget_tropical_associativity() {
1156 let a = Budget::new(100);
1157 let b = Budget::new(200);
1158 let c = Budget::new(50);
1159 assert_eq!(a.min(b).min(c), a.min(b.min(c)));
1160 }
1161
1162 #[test]
1167 fn no_caps_empty() {
1168 let caps = NoCaps;
1169 assert_eq!(caps.count(), 0);
1170 assert!(caps.is_empty());
1171 assert!(caps.capability_names().is_empty());
1172 }
1173
1174 #[test]
1175 fn no_caps_clone() {
1176 let a = NoCaps;
1177 let b = a.clone();
1178 assert_eq!(a, b);
1179 }
1180
1181 #[derive(Clone, Debug)]
1186 struct TestCaps {
1187 can_read: bool,
1188 can_write: bool,
1189 }
1190
1191 impl CapabilitySet for TestCaps {
1192 fn capability_names(&self) -> alloc::vec::Vec<&str> {
1193 let mut names = alloc::vec::Vec::new();
1194 if self.can_read {
1195 names.push("read");
1196 }
1197 if self.can_write {
1198 names.push("write");
1199 }
1200 names
1201 }
1202
1203 fn count(&self) -> usize {
1204 usize::from(self.can_read) + usize::from(self.can_write)
1205 }
1206 }
1207
1208 #[derive(Clone, Debug)]
1210 struct LayeredCaps {
1211 level: u32,
1212 }
1213
1214 impl CapabilitySet for LayeredCaps {
1215 fn capability_names(&self) -> alloc::vec::Vec<&str> {
1216 if self.level > 0 {
1217 alloc::vec!["layer"]
1218 } else {
1219 alloc::vec::Vec::new()
1220 }
1221 }
1222
1223 fn count(&self) -> usize {
1224 usize::from(self.level > 0)
1225 }
1226 }
1227
1228 #[test]
1233 fn cx_root_creation() {
1234 let trace = TraceId::from_parts(1_700_000_000_000, 1);
1235 let cx = Cx::new(trace, Budget::new(5000), NoCaps);
1236 assert_eq!(cx.trace_id(), trace);
1237 assert_eq!(cx.budget().remaining_ms(), 5000);
1238 assert_eq!(cx.depth(), 0);
1239 assert!(cx.capabilities().is_empty());
1240 }
1241
1242 #[test]
1243 fn cx_child_inherits_trace() {
1244 let trace = TraceId::from_parts(1_700_000_000_000, 42);
1245 let cx = Cx::new(trace, Budget::new(5000), NoCaps);
1246 let child = cx.child(NoCaps, Budget::new(3000));
1247 assert_eq!(child.trace_id(), trace);
1248 }
1249
1250 #[test]
1251 fn cx_child_budget_takes_min() {
1252 let cx = Cx::new(TraceId::from_raw(1), Budget::new(2000), NoCaps);
1253 let child1 = cx.child(NoCaps, Budget::new(1000));
1254 assert_eq!(child1.budget().remaining_ms(), 1000);
1255 let child2 = cx.child(NoCaps, Budget::new(5000));
1256 assert_eq!(child2.budget().remaining_ms(), 2000);
1257 }
1258
1259 #[test]
1260 fn cx_child_increments_depth() {
1261 let cx = Cx::new(TraceId::from_raw(1), Budget::new(1000), NoCaps);
1262 let child = cx.child(NoCaps, Budget::new(1000));
1263 assert_eq!(child.depth(), 1);
1264 let grandchild = child.child(NoCaps, Budget::new(1000));
1265 assert_eq!(grandchild.depth(), 2);
1266 }
1267
1268 #[test]
1269 fn cx_consume_budget() {
1270 let mut cx = Cx::new(TraceId::from_raw(1), Budget::new(500), NoCaps);
1271 assert!(cx.consume_budget(200));
1272 assert_eq!(cx.budget().remaining_ms(), 300);
1273 assert!(!cx.consume_budget(400));
1274 assert_eq!(cx.budget().remaining_ms(), 300);
1275 }
1276
1277 #[test]
1278 fn cx_debug_format() {
1279 let cx = Cx::new(TraceId::from_raw(0xAB), Budget::new(100), NoCaps);
1280 let dbg = std::format!("{cx:?}");
1281 assert!(dbg.contains("Cx"));
1282 assert!(dbg.contains("budget_ms"));
1283 assert!(dbg.contains("100"));
1284 }
1285
1286 #[test]
1287 fn cx_with_custom_capabilities() {
1288 let caps = TestCaps {
1289 can_read: true,
1290 can_write: false,
1291 };
1292 let cx = Cx::new(TraceId::from_raw(1), Budget::new(1000), caps);
1293 assert_eq!(cx.capabilities().count(), 1);
1294 assert_eq!(cx.capabilities().capability_names(), &["read"]);
1295 }
1296
1297 #[test]
1298 fn cx_child_with_attenuated_capabilities() {
1299 let full_caps = TestCaps {
1300 can_read: true,
1301 can_write: true,
1302 };
1303 let cx = Cx::new(TraceId::from_raw(1), Budget::new(1000), full_caps);
1304 assert_eq!(cx.capabilities().count(), 2);
1305
1306 let read_only = TestCaps {
1307 can_read: true,
1308 can_write: false,
1309 };
1310 let child = cx.child(read_only, Budget::new(500));
1311 assert_eq!(child.capabilities().count(), 1);
1312 assert!(!child.capabilities().capability_names().contains(&"write"));
1313 }
1314
1315 #[test]
1316 fn cx_capability_attenuation_chain_10x() {
1317 let trace = TraceId::from_raw(0x42);
1319 let root = Cx::new(trace, Budget::new(10_000), LayeredCaps { level: 10 });
1320 assert_eq!(root.capabilities().level, 10);
1321
1322 let mut prev_level = 10_u32;
1323 let child1 = root.child(LayeredCaps { level: 9 }, Budget::new(9000));
1324 assert!(child1.capabilities().level < prev_level);
1325 prev_level = child1.capabilities().level;
1326
1327 let child2 = child1.child(LayeredCaps { level: 8 }, Budget::new(8000));
1328 assert!(child2.capabilities().level < prev_level);
1329 prev_level = child2.capabilities().level;
1330
1331 let child3 = child2.child(LayeredCaps { level: 7 }, Budget::new(7000));
1332 assert!(child3.capabilities().level < prev_level);
1333 prev_level = child3.capabilities().level;
1334
1335 let child4 = child3.child(LayeredCaps { level: 6 }, Budget::new(6000));
1336 assert!(child4.capabilities().level < prev_level);
1337 prev_level = child4.capabilities().level;
1338
1339 let child5 = child4.child(LayeredCaps { level: 5 }, Budget::new(5000));
1340 assert!(child5.capabilities().level < prev_level);
1341 prev_level = child5.capabilities().level;
1342
1343 let child6 = child5.child(LayeredCaps { level: 4 }, Budget::new(4000));
1344 assert!(child6.capabilities().level < prev_level);
1345 prev_level = child6.capabilities().level;
1346
1347 let child7 = child6.child(LayeredCaps { level: 3 }, Budget::new(3000));
1348 assert!(child7.capabilities().level < prev_level);
1349 prev_level = child7.capabilities().level;
1350
1351 let child8 = child7.child(LayeredCaps { level: 2 }, Budget::new(2000));
1352 assert!(child8.capabilities().level < prev_level);
1353 prev_level = child8.capabilities().level;
1354
1355 let child9 = child8.child(LayeredCaps { level: 1 }, Budget::new(1000));
1356 assert!(child9.capabilities().level < prev_level);
1357 prev_level = child9.capabilities().level;
1358
1359 let child10 = child9.child(LayeredCaps { level: 0 }, Budget::new(500));
1360 assert!(child10.capabilities().level < prev_level);
1361 assert_eq!(child10.capabilities().level, 0);
1362 assert!(child10.capabilities().is_empty());
1363 assert_eq!(child10.depth(), 10);
1364
1365 assert_eq!(child10.trace_id(), trace);
1367 assert_eq!(child10.budget().remaining_ms(), 500);
1369 }
1370
1371 #[test]
1372 fn cx_deep_nesting_budget_monotonic() {
1373 let cx = Cx::new(TraceId::from_raw(1), Budget::new(1000), NoCaps);
1375 let c1 = cx.child(NoCaps, Budget::new(900));
1376 let c2 = c1.child(NoCaps, Budget::new(800));
1377 let c3 = c2.child(NoCaps, Budget::new(700));
1378 let c4 = c3.child(NoCaps, Budget::new(600));
1379
1380 assert!(c1.budget().remaining_ms() <= cx.budget().remaining_ms());
1381 assert!(c2.budget().remaining_ms() <= c1.budget().remaining_ms());
1382 assert!(c3.budget().remaining_ms() <= c2.budget().remaining_ms());
1383 assert!(c4.budget().remaining_ms() <= c3.budget().remaining_ms());
1384 }
1385
1386 #[test]
1387 fn cx_child_cannot_exceed_parent_budget() {
1388 let cx = Cx::new(TraceId::from_raw(1), Budget::new(100), NoCaps);
1389 let child = cx.child(NoCaps, Budget::UNLIMITED);
1391 assert_eq!(child.budget().remaining_ms(), 100);
1392 }
1393
1394 #[test]
1395 fn cx_trace_propagation_through_chain() {
1396 let trace = TraceId::from_parts(1_700_000_000_000, 0xCAFE);
1397 let cx = Cx::new(trace, Budget::UNLIMITED, NoCaps);
1398 let c1 = cx.child(NoCaps, Budget::UNLIMITED);
1399 let c2 = c1.child(NoCaps, Budget::UNLIMITED);
1400 let c3 = c2.child(NoCaps, Budget::UNLIMITED);
1401 assert_eq!(c3.trace_id(), trace);
1402 assert_eq!(c3.depth(), 3);
1403 }
1404
1405 #[test]
1410 fn _97gwup_from_parts_within_range_round_trips() {
1411 let ts = 1_700_000_000_000;
1413 let trace = TraceId::from_parts(ts, 0xDEAD_BEEF);
1414 assert_eq!(trace.timestamp_ms(), ts);
1415 }
1416
1417 #[test]
1418 fn _97gwup_from_parts_at_48_bit_boundary() {
1419 let ts = (1u64 << 48) - 1;
1421 let trace = TraceId::from_parts(ts, 0);
1422 assert_eq!(trace.timestamp_ms(), ts);
1423 }
1424
1425 #[test]
1426 fn _97gwup_from_parts_saturates_above_48_bits() {
1427 let oversize = (1u64 << 48) + 12345;
1431 let trace = TraceId::from_parts(oversize, 0);
1432 assert_eq!(trace.timestamp_ms(), (1u64 << 48) - 1);
1433 }
1434
1435 #[test]
1436 fn _97gwup_from_parts_max_u64_saturates() {
1437 let trace = TraceId::from_parts(u64::MAX, 0);
1438 assert_eq!(trace.timestamp_ms(), (1u64 << 48) - 1);
1439 }
1440
1441 #[test]
1442 fn _97gwup_try_from_parts_within_range_ok() {
1443 let ts = 1_700_000_000_000;
1444 let trace = TraceId::try_from_parts(ts, 0xCAFE).expect("in range");
1445 assert_eq!(trace.timestamp_ms(), ts);
1446 }
1447
1448 #[test]
1449 fn _97gwup_try_from_parts_at_boundary_ok() {
1450 let ts = (1u64 << 48) - 1;
1451 assert!(TraceId::try_from_parts(ts, 0).is_ok());
1452 }
1453
1454 #[test]
1455 fn _97gwup_try_from_parts_above_boundary_err() {
1456 let ts = 1u64 << 48;
1457 let err = TraceId::try_from_parts(ts, 0).expect_err("out of range");
1458 assert_eq!(err, ts);
1459 }
1460
1461 #[test]
1462 fn _97gwup_try_from_parts_max_u64_err() {
1463 let err = TraceId::try_from_parts(u64::MAX, 0).expect_err("out of range");
1464 assert_eq!(err, u64::MAX);
1465 }
1466}
1467
1468#[cfg(test)]
1473mod proptest_tests {
1474 extern crate std;
1475
1476 use super::*;
1477 use core::hash::{Hash, Hasher};
1478 use proptest::prelude::*;
1479 use std::collections::hash_map::DefaultHasher;
1480 use std::string::ToString;
1481
1482 fn hash_of<T: Hash>(val: &T) -> u64 {
1483 let mut h = DefaultHasher::new();
1484 val.hash(&mut h);
1485 h.finish()
1486 }
1487
1488 proptest! {
1491 #[test]
1492 fn trace_id_display_fromstr_roundtrip(raw: u128) {
1493 let id = TraceId::from_raw(raw);
1494 let hex = id.to_string();
1495 let parsed: TraceId = hex.parse().unwrap();
1496 prop_assert_eq!(id, parsed);
1497 }
1498
1499 #[test]
1500 fn trace_id_serde_roundtrip(raw: u128) {
1501 let id = TraceId::from_raw(raw);
1502 let json = serde_json::to_string(&id).unwrap();
1503 let parsed: TraceId = serde_json::from_str(&json).unwrap();
1504 prop_assert_eq!(id, parsed);
1505 }
1506
1507 #[test]
1508 fn trace_id_bytes_roundtrip(raw: u128) {
1509 let id = TraceId::from_raw(raw);
1510 let bytes = id.to_bytes();
1511 let recovered = TraceId::from_bytes(bytes);
1512 prop_assert_eq!(id, recovered);
1513 }
1514
1515 #[test]
1516 fn trace_id_hash_consistency(a: u128, b: u128) {
1517 let id_a = TraceId::from_raw(a);
1518 let id_b = TraceId::from_raw(b);
1519 if id_a == id_b {
1520 prop_assert_eq!(hash_of(&id_a), hash_of(&id_b));
1521 }
1522 }
1523
1524 #[test]
1525 fn trace_id_from_parts_preserves_timestamp(ts_ms: u64, random: u128) {
1526 let ts_masked = ts_ms & 0xFFFF_FFFF_FFFF;
1528 let id = TraceId::from_parts(ts_masked, random);
1529 prop_assert_eq!(id.timestamp_ms(), ts_masked);
1530 }
1531 }
1532
1533 proptest! {
1536 #[test]
1537 fn decision_id_display_fromstr_roundtrip(raw: u128) {
1538 let id = DecisionId::from_raw(raw);
1539 let hex = id.to_string();
1540 let parsed: DecisionId = hex.parse().unwrap();
1541 prop_assert_eq!(id, parsed);
1542 }
1543
1544 #[test]
1545 fn decision_id_serde_roundtrip(raw: u128) {
1546 let id = DecisionId::from_raw(raw);
1547 let json = serde_json::to_string(&id).unwrap();
1548 let parsed: DecisionId = serde_json::from_str(&json).unwrap();
1549 prop_assert_eq!(id, parsed);
1550 }
1551
1552 #[test]
1553 fn decision_id_hash_consistency(a: u128, b: u128) {
1554 let id_a = DecisionId::from_raw(a);
1555 let id_b = DecisionId::from_raw(b);
1556 if id_a == id_b {
1557 prop_assert_eq!(hash_of(&id_a), hash_of(&id_b));
1558 }
1559 }
1560 }
1561
1562 proptest! {
1565 #[test]
1566 fn schema_version_parse_roundtrip(major: u32, minor: u32, patch: u32) {
1567 let v = SchemaVersion::new(major, minor, patch);
1568 let s = v.to_string();
1569 let parsed: SchemaVersion = s.parse().unwrap();
1570 prop_assert_eq!(v, parsed);
1571 }
1572
1573 #[test]
1574 fn schema_version_serde_roundtrip(major: u32, minor: u32, patch: u32) {
1575 let v = SchemaVersion::new(major, minor, patch);
1576 let json = serde_json::to_string(&v).unwrap();
1577 let parsed: SchemaVersion = serde_json::from_str(&json).unwrap();
1578 prop_assert_eq!(v, parsed);
1579 }
1580
1581 #[test]
1582 fn schema_version_compatible_reflexive(major: u32, minor: u32, patch: u32) {
1583 let v = SchemaVersion::new(major, minor, patch);
1584 prop_assert!(v.is_compatible(&v));
1585 }
1586
1587 #[test]
1588 fn schema_version_compatible_symmetric(
1589 m1: u32, n1: u32, p1: u32,
1590 m2: u32, n2: u32, p2: u32
1591 ) {
1592 let a = SchemaVersion::new(m1, n1, p1);
1593 let b = SchemaVersion::new(m2, n2, p2);
1594 prop_assert_eq!(a.is_compatible(&b), b.is_compatible(&a));
1595 }
1596
1597 #[test]
1598 fn schema_version_compatible_transitive(
1599 m1: u32, n1: u32, p1: u32,
1600 n2: u32, p2: u32,
1601 n3: u32, p3: u32
1602 ) {
1603 let a = SchemaVersion::new(m1, n1, p1);
1606 let b = SchemaVersion::new(m1, n2, p2);
1607 let c = SchemaVersion::new(m1, n3, p3);
1608 if a.is_compatible(&b) && b.is_compatible(&c) {
1609 prop_assert!(a.is_compatible(&c));
1610 }
1611 }
1612
1613 #[test]
1614 fn schema_version_hash_consistency(
1615 m1: u32, n1: u32, p1: u32,
1616 m2: u32, n2: u32, p2: u32
1617 ) {
1618 let a = SchemaVersion::new(m1, n1, p1);
1619 let b = SchemaVersion::new(m2, n2, p2);
1620 if a == b {
1621 prop_assert_eq!(hash_of(&a), hash_of(&b));
1622 }
1623 }
1624 }
1625
1626 proptest! {
1629 #[test]
1630 fn budget_min_commutative(a: u64, b: u64) {
1631 let ba = Budget::new(a);
1632 let bb = Budget::new(b);
1633 prop_assert_eq!(ba.min(bb), bb.min(ba));
1634 }
1635
1636 #[test]
1637 fn budget_min_associative(a: u64, b: u64, c: u64) {
1638 let ba = Budget::new(a);
1639 let bb = Budget::new(b);
1640 let bc = Budget::new(c);
1641 prop_assert_eq!(ba.min(bb).min(bc), ba.min(bb.min(bc)));
1642 }
1643
1644 #[test]
1645 fn budget_min_identity(a: u64) {
1646 let ba = Budget::new(a);
1648 prop_assert_eq!(ba.min(Budget::UNLIMITED), ba);
1649 prop_assert_eq!(Budget::UNLIMITED.min(ba), ba);
1650 }
1651
1652 #[test]
1653 fn budget_min_idempotent(a: u64) {
1654 let ba = Budget::new(a);
1655 prop_assert_eq!(ba.min(ba), ba);
1656 }
1657
1658 #[test]
1659 fn budget_consume_additive(total in 0..=10_000_u64, a in 0..=5_000_u64, b in 0..=5_000_u64) {
1660 let budget = Budget::new(total);
1662 if a + b <= total {
1663 let after_both = budget.consume(a + b).unwrap();
1664 let after_a = budget.consume(a).unwrap();
1665 let after_ab = after_a.consume(b).unwrap();
1666 prop_assert_eq!(after_both.remaining_ms(), after_ab.remaining_ms());
1667 }
1668 }
1669
1670 #[test]
1671 fn budget_serde_roundtrip(ms: u64) {
1672 let b = Budget::new(ms);
1673 let json = serde_json::to_string(&b).unwrap();
1674 let parsed: Budget = serde_json::from_str(&json).unwrap();
1675 prop_assert_eq!(b, parsed);
1676 }
1677
1678 #[test]
1679 fn budget_hash_consistency(a: u64, b: u64) {
1680 let ba = Budget::new(a);
1681 let bb = Budget::new(b);
1682 if ba == bb {
1683 prop_assert_eq!(hash_of(&ba), hash_of(&bb));
1684 }
1685 }
1686 }
1687
1688 proptest! {
1691 #[test]
1692 fn cx_child_budget_never_exceeds_parent(parent_ms: u64, child_ms: u64) {
1693 let trace = TraceId::from_raw(1);
1694 let cx = Cx::new(trace, Budget::new(parent_ms), NoCaps);
1695 let child = cx.child(NoCaps, Budget::new(child_ms));
1696 prop_assert!(child.budget().remaining_ms() <= cx.budget().remaining_ms());
1697 }
1698
1699 #[test]
1700 fn cx_child_trace_always_inherited(raw: u128, budget_ms: u64) {
1701 let trace = TraceId::from_raw(raw);
1702 let cx = Cx::new(trace, Budget::new(budget_ms), NoCaps);
1703 let child = cx.child(NoCaps, Budget::new(budget_ms));
1704 prop_assert_eq!(child.trace_id(), trace);
1705 }
1706
1707 #[test]
1708 fn cx_child_depth_increments(raw: u128, budget_ms: u64) {
1709 let cx = Cx::new(TraceId::from_raw(raw), Budget::new(budget_ms), NoCaps);
1710 let child = cx.child(NoCaps, Budget::new(budget_ms));
1711 prop_assert_eq!(child.depth(), cx.depth() + 1);
1712 }
1713 }
1714}