1use core::fmt;
39
40#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
56#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
57#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
58pub enum PreemptionReason {
59 #[cfg_attr(feature = "serde", serde(alias = "PriorityPreemption"))]
62 PriorityPreemption,
63 #[cfg_attr(feature = "serde", serde(alias = "QuotaRebalance"))]
67 QuotaRebalance,
68 #[cfg_attr(feature = "serde", serde(alias = "BurstCreditExhausted"))]
73 BurstCreditExhausted,
74 #[cfg_attr(feature = "serde", serde(alias = "BudgetExhausted"))]
77 BudgetExhausted,
78 #[cfg_attr(feature = "serde", serde(alias = "CostCapEviction"))]
81 CostCapEviction,
82 #[cfg_attr(feature = "serde", serde(alias = "OperatorDrain"))]
84 OperatorDrain,
85 #[cfg_attr(feature = "serde", serde(alias = "OperatorMigProfileChange"))]
90 OperatorMigProfileChange,
91 #[cfg_attr(feature = "serde", serde(alias = "MaintenanceWindow"))]
94 MaintenanceWindow,
95 #[cfg_attr(feature = "serde", serde(alias = "PolicyViolationRecovery"))]
100 PolicyViolationRecovery,
101}
102
103impl PreemptionReason {
104 pub fn as_str(self) -> &'static str {
108 match self {
109 Self::PriorityPreemption => "priority_preemption",
110 Self::QuotaRebalance => "quota_rebalance",
111 Self::BurstCreditExhausted => "burst_credit_exhausted",
112 Self::BudgetExhausted => "budget_exhausted",
113 Self::CostCapEviction => "cost_cap_eviction",
114 Self::OperatorDrain => "operator_drain",
115 Self::OperatorMigProfileChange => "operator_mig_profile_change",
116 Self::MaintenanceWindow => "maintenance_window",
117 Self::PolicyViolationRecovery => "policy_violation_recovery",
118 }
119 }
120
121 pub fn human_summary(self) -> &'static str {
140 match self {
141 Self::PriorityPreemption => "reclaimed for higher-priority work that outranked it",
142 Self::QuotaRebalance => {
143 "reclaimed because tenant fair-share moved a different tenant in"
144 }
145 Self::BurstCreditExhausted => {
146 "reclaimed because the tenant's burst-bucket envelope was exceeded"
147 }
148 Self::BudgetExhausted => "reclaimed because the tenant's spend budget was exhausted",
149 Self::CostCapEviction => {
150 "reclaimed because the global cost cap could no longer be satisfied"
151 }
152 Self::OperatorDrain => "reclaimed because an operator drained the host node",
153 Self::OperatorMigProfileChange => {
154 "reclaimed because an operator recomposed the GPU MIG profile"
155 }
156 Self::MaintenanceWindow => "reclaimed by a scheduled maintenance window",
157 Self::PolicyViolationRecovery => {
158 "reclaimed because the workload no longer satisfied a mandatory policy"
159 }
160 }
161 }
162}
163
164impl fmt::Display for PreemptionReason {
165 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166 f.write_str(self.as_str())
167 }
168}
169
170#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
200#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
201#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
202pub enum Priority {
203 #[cfg_attr(feature = "serde", serde(alias = "Scavenger"))]
205 Scavenger = 0,
206 #[cfg_attr(feature = "serde", serde(alias = "Standard"))]
208 Standard = 1,
209 #[cfg_attr(feature = "serde", serde(alias = "Guaranteed"))]
211 Guaranteed = 2,
212}
213
214impl Priority {
215 pub fn as_str(self) -> &'static str {
219 match self {
220 Self::Scavenger => "scavenger",
221 Self::Standard => "standard",
222 Self::Guaranteed => "guaranteed",
223 }
224 }
225
226 pub fn human_summary(self) -> &'static str {
233 match self {
234 Self::Scavenger => "uses leftover capacity; preempted first when contention rises",
235 Self::Standard => "uses unreserved capacity; preemptible by guaranteed work",
236 Self::Guaranteed => "reserved capacity; never preempted by ordinary scheduling",
237 }
238 }
239}
240
241impl fmt::Display for Priority {
242 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243 f.write_str(self.as_str())
244 }
245}
246
247impl PartialOrd for Priority {
248 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
249 Some(self.cmp(other))
250 }
251}
252
253impl Ord for Priority {
254 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
255 (*self as u8).cmp(&(*other as u8))
256 }
257}
258
259#[derive(Clone, Debug, PartialEq, Eq)]
275pub struct ParsePriorityError(pub String);
276
277impl fmt::Display for ParsePriorityError {
278 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
279 write!(
280 f,
281 "invalid priority {:?} — valid values: scavenger | standard | guaranteed",
282 self.0
283 )
284 }
285}
286
287impl std::error::Error for ParsePriorityError {}
288
289impl core::str::FromStr for Priority {
290 type Err = ParsePriorityError;
291
292 fn from_str(s: &str) -> Result<Self, Self::Err> {
324 match s {
325 "scavenger" => Ok(Self::Scavenger),
326 "standard" => Ok(Self::Standard),
327 "guaranteed" => Ok(Self::Guaranteed),
328 "best_effort" => Ok(Self::Standard),
330 _ => Err(ParsePriorityError(s.to_string())),
331 }
332 }
333}
334
335#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
351#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
352#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
353pub enum EvidenceLabel {
354 #[cfg_attr(feature = "serde", serde(alias = "DesignTarget"))]
356 DesignTarget,
357 #[cfg_attr(feature = "serde", serde(alias = "UnitIntegrationEvidence"))]
359 UnitIntegrationEvidence,
360 #[cfg_attr(feature = "serde", serde(alias = "LabEvidence"))]
362 LabEvidence,
363 #[cfg_attr(feature = "serde", serde(alias = "StagedProviderEvidence"))]
366 StagedProviderEvidence,
367 #[cfg_attr(feature = "serde", serde(alias = "DesignPartnerEvidence"))]
369 DesignPartnerEvidence,
370 #[cfg_attr(feature = "serde", serde(alias = "ProductionEvidence"))]
372 ProductionEvidence,
373}
374
375impl EvidenceLabel {
376 pub fn as_str(self) -> &'static str {
377 match self {
378 Self::DesignTarget => "design target",
379 Self::UnitIntegrationEvidence => "unit/integration evidence",
380 Self::LabEvidence => "lab evidence",
381 Self::StagedProviderEvidence => "staged provider evidence",
382 Self::DesignPartnerEvidence => "design-partner evidence",
383 Self::ProductionEvidence => "production evidence",
384 }
385 }
386
387 pub fn requires_artifact(self) -> bool {
391 !matches!(self, Self::DesignTarget)
392 }
393}
394
395impl fmt::Display for EvidenceLabel {
396 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
397 f.write_str(self.as_str())
398 }
399}
400
401#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
433#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
434#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
435pub enum RejectionReason {
436 #[cfg_attr(feature = "serde", serde(alias = "InsufficientCapacity"))]
439 InsufficientCapacity,
440 #[cfg_attr(feature = "serde", serde(alias = "NoEligibleNodes"))]
444 NoEligibleNodes,
445 #[cfg_attr(feature = "serde", serde(alias = "QuotaHardLimitExceeded"))]
448 QuotaHardLimitExceeded,
449 #[cfg_attr(feature = "serde", serde(alias = "QuotaBurstExceeded"))]
453 QuotaBurstExceeded,
454 #[cfg_attr(feature = "serde", serde(alias = "QuotaLeaseCountExceeded"))]
457 QuotaLeaseCountExceeded,
458 #[cfg_attr(feature = "serde", serde(alias = "QuotaPerNodeLimitExceeded"))]
461 QuotaPerNodeLimitExceeded,
462 #[cfg_attr(feature = "serde", serde(alias = "TenantNotFound"))]
467 TenantNotFound,
468 #[cfg_attr(feature = "serde", serde(alias = "NodeFenced"))]
471 NodeFenced,
472 #[cfg_attr(feature = "serde", serde(alias = "BudgetExhausted"))]
475 BudgetExhausted,
476 #[cfg_attr(feature = "serde", serde(alias = "ReservationExhausted"))]
479 ReservationExhausted,
480 #[cfg_attr(feature = "serde", serde(alias = "PlacementPolicyExcluded"))]
489 PlacementPolicyExcluded,
490 #[cfg_attr(feature = "serde", serde(alias = "NodeAddressUnknown"))]
496 NodeAddressUnknown,
497 #[cfg_attr(feature = "serde", serde(alias = "NodeDrained"))]
512 NodeDrained,
513 #[cfg_attr(feature = "serde", serde(alias = "EmptyBundle"))]
517 EmptyBundle,
518 #[cfg_attr(feature = "serde", serde(alias = "InsufficientBundleCapacity"))]
521 InsufficientBundleCapacity,
522 #[cfg_attr(feature = "serde", serde(alias = "BundleConstraintViolation"))]
525 BundleConstraintViolation,
526 #[cfg_attr(feature = "serde", serde(alias = "HardwareGenerationUnavailable"))]
529 HardwareGenerationUnavailable,
530 #[cfg_attr(feature = "serde", serde(alias = "OtherBundleFailure"))]
534 OtherBundleFailure,
535}
536
537impl RejectionReason {
538 pub fn as_str(self) -> &'static str {
543 match self {
544 Self::InsufficientCapacity => "insufficient_capacity",
545 Self::NoEligibleNodes => "no_eligible_nodes",
546 Self::QuotaHardLimitExceeded => "quota_hard_limit_exceeded",
547 Self::QuotaBurstExceeded => "quota_burst_exceeded",
548 Self::QuotaLeaseCountExceeded => "quota_lease_count_exceeded",
549 Self::QuotaPerNodeLimitExceeded => "quota_per_node_limit_exceeded",
550 Self::TenantNotFound => "tenant_not_found",
551 Self::NodeFenced => "node_fenced",
552 Self::BudgetExhausted => "budget_exhausted",
553 Self::ReservationExhausted => "reservation_exhausted",
554 Self::PlacementPolicyExcluded => "placement_policy_excluded",
555 Self::NodeAddressUnknown => "node_address_unknown",
556 Self::NodeDrained => "node_drained",
557 Self::EmptyBundle => "empty_bundle",
558 Self::InsufficientBundleCapacity => "insufficient_bundle_capacity",
559 Self::BundleConstraintViolation => "bundle_constraint_violation",
560 Self::HardwareGenerationUnavailable => "hardware_generation_unavailable",
561 Self::OtherBundleFailure => "other_bundle_failure",
562 }
563 }
564
565 pub fn human_summary(self) -> &'static str {
582 match self {
583 Self::InsufficientCapacity => "insufficient free capacity across all eligible nodes",
584 Self::NoEligibleNodes => "no nodes match the placement constraint",
585 Self::QuotaHardLimitExceeded => "tenant quota hard limit reached",
586 Self::QuotaBurstExceeded => "tenant burst quota cap reached",
587 Self::QuotaLeaseCountExceeded => "tenant lease count limit reached",
588 Self::QuotaPerNodeLimitExceeded => "tenant per-node capacity limit reached",
589 Self::TenantNotFound => "tenant id not registered with the scheduler",
590 Self::NodeFenced => "selected node is fenced for the requested resource type",
591 Self::BudgetExhausted => "power or cost budget cap reached",
592 Self::ReservationExhausted => "reservation has insufficient remaining capacity",
593 Self::PlacementPolicyExcluded => "placement policy excluded this candidate",
594 Self::NodeAddressUnknown => {
595 "scheduler does not have a control-plane address for this node"
596 }
597 Self::NodeDrained => {
598 "target node is drained for maintenance; admission resumes after the node returns to accepting mode"
599 }
600 Self::EmptyBundle => "bundle had zero requirements; nothing to admit",
601 Self::InsufficientBundleCapacity => {
602 "insufficient capacity for at least one bundle requirement"
603 }
604 Self::BundleConstraintViolation => {
605 "bundle placement constraint excludes every viable candidate plan"
606 }
607 Self::HardwareGenerationUnavailable => {
608 "requested bundle hardware generation has no matching inventory"
609 }
610 Self::OtherBundleFailure => "other bundle admission failure",
611 }
612 }
613}
614
615impl fmt::Display for RejectionReason {
616 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
617 f.write_str(self.as_str())
618 }
619}
620
621#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
639#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
640#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
641pub enum EgressEnforcement {
642 #[cfg_attr(feature = "serde", serde(alias = "FabricEnforced"))]
648 FabricEnforced,
649 #[cfg_attr(feature = "serde", serde(alias = "HostRuntimeIntegration"))]
655 HostRuntimeIntegration,
656 #[cfg_attr(feature = "serde", serde(alias = "OperatorControlled"))]
662 OperatorControlled,
663 #[cfg_attr(feature = "serde", serde(alias = "Unsupported"))]
671 Unsupported,
672}
673
674impl EgressEnforcement {
675 pub fn as_str(self) -> &'static str {
679 match self {
680 Self::FabricEnforced => "fabric_enforced",
681 Self::HostRuntimeIntegration => "host_runtime_integration",
682 Self::OperatorControlled => "operator_controlled",
683 Self::Unsupported => "unsupported",
684 }
685 }
686}
687
688impl fmt::Display for EgressEnforcement {
689 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
690 f.write_str(self.as_str())
691 }
692}
693
694#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
705#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
706#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
707pub enum EgressTarget {
708 #[cfg_attr(feature = "serde", serde(alias = "WasmTasklet"))]
711 WasmTasklet,
712 #[cfg_attr(feature = "serde", serde(alias = "NativeProgram"))]
716 NativeProgram,
717 #[cfg_attr(feature = "serde", serde(alias = "ContainerAdjacent"))]
721 ContainerAdjacent,
722 #[cfg_attr(feature = "serde", serde(alias = "KubernetesDra"))]
727 KubernetesDra,
728 #[cfg_attr(feature = "serde", serde(alias = "BareMetal"))]
733 BareMetal,
734}
735
736impl EgressTarget {
737 pub fn as_str(self) -> &'static str {
739 match self {
740 Self::WasmTasklet => "wasm_tasklet",
741 Self::NativeProgram => "native_program",
742 Self::ContainerAdjacent => "container_adjacent",
743 Self::KubernetesDra => "kubernetes_dra",
744 Self::BareMetal => "bare_metal",
745 }
746 }
747
748 pub fn enforcement(self) -> EgressEnforcement {
762 match self {
763 Self::WasmTasklet => EgressEnforcement::FabricEnforced,
764 Self::NativeProgram => EgressEnforcement::OperatorControlled,
765 Self::ContainerAdjacent => EgressEnforcement::OperatorControlled,
766 Self::KubernetesDra => EgressEnforcement::OperatorControlled,
767 Self::BareMetal => EgressEnforcement::HostRuntimeIntegration,
768 }
769 }
770}
771
772impl fmt::Display for EgressTarget {
773 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
774 f.write_str(self.as_str())
775 }
776}
777
778#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
797#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
798#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
799pub enum EconomicsSource {
800 #[cfg_attr(feature = "serde", serde(alias = "OperatorStaticConfig"))]
804 OperatorStaticConfig,
805 #[cfg_attr(feature = "serde", serde(alias = "ProviderPriceSheet"))]
809 ProviderPriceSheet,
810 #[cfg_attr(feature = "serde", serde(alias = "ProviderUsageExport"))]
815 ProviderUsageExport,
816 #[cfg_attr(feature = "serde", serde(alias = "MeasuredPowerTelemetry"))]
820 MeasuredPowerTelemetry,
821 #[cfg_attr(feature = "serde", serde(alias = "ThirdPartyCarbonFeed"))]
824 ThirdPartyCarbonFeed,
825 #[cfg_attr(feature = "serde", serde(alias = "DerivedEstimate"))]
829 DerivedEstimate,
830}
831
832impl EconomicsSource {
833 pub fn as_str(self) -> &'static str {
835 match self {
836 Self::OperatorStaticConfig => "operator_static_config",
837 Self::ProviderPriceSheet => "provider_price_sheet",
838 Self::ProviderUsageExport => "provider_usage_export",
839 Self::MeasuredPowerTelemetry => "measured_power_telemetry",
840 Self::ThirdPartyCarbonFeed => "third_party_carbon_feed",
841 Self::DerivedEstimate => "derived_estimate",
842 }
843 }
844}
845
846impl fmt::Display for EconomicsSource {
847 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
848 f.write_str(self.as_str())
849 }
850}
851
852#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
862#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
863#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
864pub enum ObservationConfidence {
865 #[cfg_attr(feature = "serde", serde(alias = "Low"))]
870 Low,
871 #[cfg_attr(feature = "serde", serde(alias = "Medium"))]
876 Medium,
877 #[cfg_attr(feature = "serde", serde(alias = "High"))]
882 High,
883}
884
885impl ObservationConfidence {
886 pub fn as_str(self) -> &'static str {
888 match self {
889 Self::Low => "low",
890 Self::Medium => "medium",
891 Self::High => "high",
892 }
893 }
894}
895
896impl fmt::Display for ObservationConfidence {
897 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
898 f.write_str(self.as_str())
899 }
900}
901
902#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
917#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
918pub struct EconomicsGeneration(pub u64);
919
920impl EconomicsGeneration {
921 pub const UNANCHORED: Self = Self(0);
924
925 pub fn next(self) -> Self {
930 Self(self.0.saturating_add(1))
931 }
932}
933
934impl fmt::Display for EconomicsGeneration {
935 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
936 write!(f, "gen={}", self.0)
937 }
938}
939
940#[derive(Clone, Debug, PartialEq, Eq)]
962#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
963pub struct EconomicsObservation<T> {
964 pub value: T,
965 pub source: EconomicsSource,
966 pub confidence: ObservationConfidence,
967 pub observed_at: u64,
970 pub valid_until: u64,
974 pub generation: EconomicsGeneration,
975}
976
977impl<T> EconomicsObservation<T> {
978 pub fn is_fresh(&self, now: u64) -> bool {
983 self.observed_at <= now && now < self.valid_until
984 }
985
986 pub fn meets_floor(&self, min: ObservationConfidence) -> bool {
990 self.confidence >= min
991 }
992
993 pub fn admissible(&self, now: u64, min: ObservationConfidence) -> bool {
997 self.is_fresh(now) && self.meets_floor(min)
998 }
999}
1000
1001#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1039#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1040pub struct FairShareWindow {
1041 pub tenant_id: u128,
1043 pub priority_class_weight: u32,
1049 pub window_secs: u64,
1052 pub min_share_secs: u64,
1055 pub max_share_secs: u64,
1058 pub generation: EconomicsGeneration,
1062}
1063
1064impl FairShareWindow {
1065 pub fn is_valid(&self) -> bool {
1073 self.min_share_secs <= self.max_share_secs
1074 && self.priority_class_weight > 0
1075 && self.window_secs > 0
1076 }
1077}
1078
1079#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1087#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1088#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
1089pub enum FairShareScope {
1090 #[cfg_attr(feature = "serde", serde(alias = "Tenant"))]
1092 Tenant { tenant_id: u128 },
1093 #[cfg_attr(feature = "serde", serde(alias = "Project"))]
1095 Project { project_id: u128 },
1096 #[cfg_attr(feature = "serde", serde(alias = "PriorityClass"))]
1098 PriorityClass { priority: Priority },
1099}
1100
1101impl FairShareScope {
1102 pub fn kind(self) -> &'static str {
1105 match self {
1106 Self::Tenant { .. } => "tenant",
1107 Self::Project { .. } => "project",
1108 Self::PriorityClass { .. } => "priority_class",
1109 }
1110 }
1111}
1112
1113impl fmt::Display for FairShareScope {
1114 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1115 f.write_str(self.kind())
1116 }
1117}
1118
1119#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1125#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1126pub struct FairShareWeight {
1127 pub scope: FairShareScope,
1128 pub weight: u32,
1129 pub min_share_secs: u64,
1130 pub max_share_secs: u64,
1131}
1132
1133impl FairShareWeight {
1134 pub fn is_valid(&self) -> bool {
1136 self.weight > 0 && self.min_share_secs <= self.max_share_secs
1137 }
1138}
1139
1140#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1147#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1148pub struct WeightedFairSharePolicy {
1149 pub window_secs: u64,
1151 pub generation: EconomicsGeneration,
1153 pub entries: Vec<FairShareWeight>,
1157}
1158
1159impl WeightedFairSharePolicy {
1160 pub fn is_valid(&self) -> bool {
1164 if self.window_secs == 0 || self.entries.is_empty() {
1165 return false;
1166 }
1167 for (idx, entry) in self.entries.iter().enumerate() {
1168 if !entry.is_valid() {
1169 return false;
1170 }
1171 if self.entries[..idx]
1172 .iter()
1173 .any(|prior| prior.scope == entry.scope)
1174 {
1175 return false;
1176 }
1177 }
1178 true
1179 }
1180
1181 pub fn total_weight(&self) -> u64 {
1185 self.entries
1186 .iter()
1187 .filter(|entry| entry.weight > 0)
1188 .map(|entry| u64::from(entry.weight))
1189 .sum()
1190 }
1191}
1192
1193#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1230#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1231#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
1232pub enum RevokeState {
1233 #[cfg_attr(feature = "serde", serde(alias = "Active"))]
1235 Active,
1236 #[cfg_attr(feature = "serde", serde(alias = "RevokeWarning"))]
1238 RevokeWarning,
1239 #[cfg_attr(feature = "serde", serde(alias = "GraceRunning"))]
1242 GraceRunning,
1243 #[cfg_attr(feature = "serde", serde(alias = "CheckpointReported"))]
1246 CheckpointReported,
1247 #[cfg_attr(feature = "serde", serde(alias = "ForcedTeardown"))]
1251 ForcedTeardown,
1252 #[cfg_attr(feature = "serde", serde(alias = "Torndown"))]
1255 Torndown,
1256 #[cfg_attr(feature = "serde", serde(alias = "Expired"))]
1261 Expired,
1262 #[cfg_attr(feature = "serde", serde(alias = "Fenced"))]
1265 Fenced,
1266 #[cfg_attr(feature = "serde", serde(alias = "FailedClosed"))]
1271 FailedClosed,
1272}
1273
1274impl RevokeState {
1275 pub fn as_str(self) -> &'static str {
1279 match self {
1280 Self::Active => "active",
1281 Self::RevokeWarning => "revoke_warning",
1282 Self::GraceRunning => "grace_running",
1283 Self::CheckpointReported => "checkpoint_reported",
1284 Self::ForcedTeardown => "forced_teardown",
1285 Self::Torndown => "torndown",
1286 Self::Expired => "expired",
1287 Self::Fenced => "fenced",
1288 Self::FailedClosed => "failed_closed",
1289 }
1290 }
1291
1292 pub fn human_summary(self) -> &'static str {
1299 match self {
1300 Self::Active => "lease is active; no revoke pending",
1301 Self::RevokeWarning => "revoke initiated; workload has been notified",
1302 Self::GraceRunning => "grace period running; workload may checkpoint",
1303 Self::CheckpointReported => "workload reported checkpoint; cooperative teardown",
1304 Self::ForcedTeardown => "forced teardown in progress; no grace or grace exceeded",
1305 Self::Torndown => "teardown complete; lease released",
1306 Self::Expired => "lease TTL aged out without revoke",
1307 Self::Fenced => "teardown failed; resource fenced for forensic clearing",
1308 Self::FailedClosed => "fail-closed terminal; lease invariant violated",
1309 }
1310 }
1311
1312 pub fn is_terminal(self) -> bool {
1320 matches!(
1321 self,
1322 Self::Torndown | Self::Fenced | Self::FailedClosed | Self::Expired
1323 )
1324 }
1325
1326 pub fn legal_transition_to(self, next: Self) -> bool {
1358 use RevokeState::*;
1359 matches!(
1360 (self, next),
1361 (Active, RevokeWarning)
1363 | (Active, ForcedTeardown)
1364 | (Active, Expired)
1365 | (Active, Fenced)
1366 | (RevokeWarning, GraceRunning)
1368 | (GraceRunning, CheckpointReported)
1370 | (CheckpointReported, Torndown)
1372 | (ForcedTeardown, Torndown)
1374 | (Expired, FailedClosed)
1376 | (CheckpointReported, Fenced)
1379 | (ForcedTeardown, Fenced)
1380 | (Active, FailedClosed)
1383 | (RevokeWarning, FailedClosed)
1384 | (GraceRunning, FailedClosed)
1385 | (CheckpointReported, FailedClosed)
1386 | (ForcedTeardown, FailedClosed)
1387 )
1388 }
1389}
1390
1391impl fmt::Display for RevokeState {
1392 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1393 f.write_str(self.as_str())
1394 }
1395}
1396
1397#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1407#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1408#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
1409pub struct RevokeGracePolicy {
1410 pub grace_secs: u64,
1413 pub max_grace_secs: u64,
1416 pub checkpoint_hooks_allowed: bool,
1419}
1420
1421impl RevokeGracePolicy {
1422 pub const fn hard_revoke() -> Self {
1423 Self {
1424 grace_secs: 0,
1425 max_grace_secs: 0,
1426 checkpoint_hooks_allowed: false,
1427 }
1428 }
1429
1430 pub fn bounded(
1431 grace_secs: u64,
1432 max_grace_secs: u64,
1433 checkpoint_hooks_allowed: bool,
1434 ) -> Result<Self, RevokeGracePolicyError> {
1435 let policy = Self {
1436 grace_secs,
1437 max_grace_secs,
1438 checkpoint_hooks_allowed,
1439 };
1440 policy.validate()?;
1441 Ok(policy)
1442 }
1443
1444 pub fn validate(self) -> Result<(), RevokeGracePolicyError> {
1445 if self.grace_secs > self.max_grace_secs {
1446 return Err(RevokeGracePolicyError::GraceExceedsMaximum);
1447 }
1448 if self.grace_secs == 0 && self.checkpoint_hooks_allowed {
1449 return Err(RevokeGracePolicyError::CheckpointHooksRequireGrace);
1450 }
1451 Ok(())
1452 }
1453
1454 pub fn is_hard_revoke(self) -> bool {
1455 self.grace_secs == 0
1456 }
1457
1458 pub fn checkpoint_hooks_allowed(self) -> bool {
1459 self.checkpoint_hooks_allowed && !self.is_hard_revoke()
1460 }
1461
1462 pub fn grace_deadline_unix_secs(
1463 self,
1464 start_unix_secs: u64,
1465 ) -> Result<Option<u64>, RevokeGracePolicyError> {
1466 self.validate()?;
1467 if self.is_hard_revoke() {
1468 return Ok(None);
1469 }
1470 start_unix_secs
1471 .checked_add(self.grace_secs)
1472 .map(Some)
1473 .ok_or(RevokeGracePolicyError::DeadlineOverflow)
1474 }
1475}
1476
1477impl Default for RevokeGracePolicy {
1478 fn default() -> Self {
1479 Self::hard_revoke()
1480 }
1481}
1482
1483#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1484#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1485#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
1486pub enum RevokeGracePolicyError {
1487 GraceExceedsMaximum,
1488 CheckpointHooksRequireGrace,
1489 DeadlineOverflow,
1490}
1491
1492impl RevokeGracePolicyError {
1493 pub fn as_str(self) -> &'static str {
1494 match self {
1495 Self::GraceExceedsMaximum => "grace_exceeds_maximum",
1496 Self::CheckpointHooksRequireGrace => "checkpoint_hooks_require_grace",
1497 Self::DeadlineOverflow => "deadline_overflow",
1498 }
1499 }
1500
1501 pub fn human_summary(self) -> &'static str {
1502 match self {
1503 Self::GraceExceedsMaximum => "requested grace exceeds policy maximum",
1504 Self::CheckpointHooksRequireGrace => "checkpoint hooks require a nonzero grace window",
1505 Self::DeadlineOverflow => "grace deadline overflowed unix timestamp range",
1506 }
1507 }
1508}
1509
1510impl fmt::Display for RevokeGracePolicyError {
1511 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1512 f.write_str(self.as_str())
1513 }
1514}
1515
1516#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1533#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1534#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
1535pub enum Preemptibility {
1536 #[cfg_attr(feature = "serde", serde(alias = "Preemptible"))]
1538 Preemptible,
1539 #[cfg_attr(feature = "serde", serde(alias = "Protected"))]
1544 Protected,
1545}
1546
1547impl Preemptibility {
1548 pub fn as_str(self) -> &'static str {
1549 match self {
1550 Self::Preemptible => "preemptible",
1551 Self::Protected => "protected",
1552 }
1553 }
1554 pub fn human_summary(self) -> &'static str {
1555 match self {
1556 Self::Preemptible => "accepts preemption per priority rules (default)",
1557 Self::Protected => "non-preemptible by ordinary scheduling; operator-declared",
1558 }
1559 }
1560 pub fn allows_preemption(self) -> bool {
1561 matches!(self, Self::Preemptible)
1562 }
1563}
1564
1565impl fmt::Display for Preemptibility {
1566 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1567 f.write_str(self.as_str())
1568 }
1569}
1570
1571#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1585#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1586#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
1587pub enum NonPreemptibleReason {
1588 #[cfg_attr(feature = "serde", serde(alias = "CheckpointInProgress"))]
1591 CheckpointInProgress,
1592 #[cfg_attr(feature = "serde", serde(alias = "DataPlaneActive"))]
1596 DataPlaneActive,
1597 #[cfg_attr(feature = "serde", serde(alias = "OperatorPin"))]
1600 OperatorPin,
1601 #[cfg_attr(feature = "serde", serde(alias = "AttestationLocked"))]
1606 AttestationLocked,
1607}
1608
1609impl NonPreemptibleReason {
1610 pub fn as_str(self) -> &'static str {
1611 match self {
1612 Self::CheckpointInProgress => "checkpoint_in_progress",
1613 Self::DataPlaneActive => "data_plane_active",
1614 Self::OperatorPin => "operator_pin",
1615 Self::AttestationLocked => "attestation_locked",
1616 }
1617 }
1618
1619 pub fn human_summary(self) -> &'static str {
1620 match self {
1621 Self::CheckpointInProgress => {
1622 "lease is mid-checkpoint; preempting would lose the checkpoint"
1623 }
1624 Self::DataPlaneActive => "data-plane binding active; teardown requires coordination",
1625 Self::OperatorPin => "operator manually pinned this lease as non-preemptible",
1626 Self::AttestationLocked => "attestation chain requires this specific lease holder",
1627 }
1628 }
1629}
1630
1631impl fmt::Display for NonPreemptibleReason {
1632 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1633 f.write_str(self.as_str())
1634 }
1635}
1636
1637#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
1660#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1661#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
1662pub enum HardwareGeneration {
1663 NvidiaAmpere,
1665 NvidiaHopper,
1667 NvidiaBlackwell,
1669 AmdMi300,
1671 AmdMi325,
1673 IntelGaudi,
1675 Other,
1678}
1679
1680impl HardwareGeneration {
1681 pub fn as_str(self) -> &'static str {
1685 match self {
1686 Self::NvidiaAmpere => "nvidia_ampere",
1687 Self::NvidiaHopper => "nvidia_hopper",
1688 Self::NvidiaBlackwell => "nvidia_blackwell",
1689 Self::AmdMi300 => "amd_mi300",
1690 Self::AmdMi325 => "amd_mi325",
1691 Self::IntelGaudi => "intel_gaudi",
1692 Self::Other => "other",
1693 }
1694 }
1695
1696 pub fn human_summary(self) -> &'static str {
1702 match self {
1703 Self::NvidiaAmpere => "NVIDIA Ampere (A100, A30, A40)",
1704 Self::NvidiaHopper => "NVIDIA Hopper (H100, H200)",
1705 Self::NvidiaBlackwell => "NVIDIA Blackwell (B200, GB200)",
1706 Self::AmdMi300 => "AMD CDNA3 (MI300, MI300X, MI300A)",
1707 Self::AmdMi325 => "AMD CDNA4 (MI325X and newer)",
1708 Self::IntelGaudi => "Intel Gaudi 2 / Gaudi 3",
1709 Self::Other => "other / generic accelerator family",
1710 }
1711 }
1712}
1713
1714impl fmt::Display for HardwareGeneration {
1715 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1716 f.write_str(self.as_str())
1717 }
1718}
1719
1720#[derive(Clone, Debug, PartialEq, Eq)]
1722pub struct ParseHardwareGenerationError(pub String);
1723
1724impl fmt::Display for ParseHardwareGenerationError {
1725 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1726 write!(
1727 f,
1728 "invalid hardware generation {:?} — valid values: nvidia_ampere | nvidia_hopper | nvidia_blackwell | amd_mi300 | amd_mi325 | intel_gaudi | other",
1729 self.0
1730 )
1731 }
1732}
1733
1734impl std::error::Error for ParseHardwareGenerationError {}
1735
1736impl core::str::FromStr for HardwareGeneration {
1737 type Err = ParseHardwareGenerationError;
1738
1739 fn from_str(s: &str) -> Result<Self, Self::Err> {
1740 match s {
1741 "nvidia_ampere" => Ok(Self::NvidiaAmpere),
1742 "nvidia_hopper" => Ok(Self::NvidiaHopper),
1743 "nvidia_blackwell" => Ok(Self::NvidiaBlackwell),
1744 "amd_mi300" => Ok(Self::AmdMi300),
1745 "amd_mi325" => Ok(Self::AmdMi325),
1746 "intel_gaudi" => Ok(Self::IntelGaudi),
1747 "other" => Ok(Self::Other),
1748 _ => Err(ParseHardwareGenerationError(s.to_string())),
1749 }
1750 }
1751}
1752
1753#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1762#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1763pub struct GpuGenerationQuota {
1764 pub generation: HardwareGeneration,
1766 pub count: u32,
1768}
1769
1770#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
1780#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1781pub struct QuotaSchema {
1782 pub mem_bytes: Option<u64>,
1784 pub cpu_cores: Option<u64>,
1786 pub gpu_count: Option<u32>,
1788 pub gpu_vram_bytes: Option<u64>,
1793 pub gpu_count_by_generation: Vec<GpuGenerationQuota>,
1796 pub block_bytes: Option<u64>,
1798 pub net_bps: Option<u64>,
1800 pub tasklet_concurrency: Option<u32>,
1802 pub lease_create_per_minute: Option<u32>,
1804 pub burst_credits: Option<BurstCreditPolicy>,
1807 pub fair_share: Option<FairShareWindow>,
1809}
1810
1811impl QuotaSchema {
1812 pub fn is_unlimited(&self) -> bool {
1814 self.mem_bytes.is_none()
1815 && self.cpu_cores.is_none()
1816 && self.gpu_count.is_none()
1817 && self.gpu_vram_bytes.is_none()
1818 && self.gpu_count_by_generation.is_empty()
1819 && self.block_bytes.is_none()
1820 && self.net_bps.is_none()
1821 && self.tasklet_concurrency.is_none()
1822 && self.lease_create_per_minute.is_none()
1823 }
1824
1825 pub fn limit_for_resource_kind(&self, kind: crate::ResourceKind) -> Option<u64> {
1828 match kind {
1829 crate::ResourceKind::Mem => self.mem_bytes,
1830 crate::ResourceKind::Cpu => self.cpu_cores,
1831 crate::ResourceKind::Gpu => self.total_gpu_count().map(u64::from),
1832 crate::ResourceKind::GpuMem => self.gpu_vram_bytes,
1833 crate::ResourceKind::Block => self.block_bytes,
1834 crate::ResourceKind::Net => self.net_bps,
1835 crate::ResourceKind::Tasklet => self.tasklet_concurrency.map(u64::from),
1836 }
1837 }
1838
1839 pub fn total_gpu_count(&self) -> Option<u32> {
1841 self.gpu_count.or_else(|| {
1842 if self.gpu_count_by_generation.is_empty() {
1843 None
1844 } else {
1845 Some(
1846 self.gpu_count_by_generation
1847 .iter()
1848 .fold(0u32, |acc, q| acc.saturating_add(q.count)),
1849 )
1850 }
1851 })
1852 }
1853
1854 pub fn gpu_limit_for_generation(&self, generation: HardwareGeneration) -> Option<u32> {
1856 self.gpu_count_by_generation
1857 .iter()
1858 .find(|q| q.generation == generation)
1859 .map(|q| q.count)
1860 }
1861}
1862
1863#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1865#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1866pub struct BurstCreditPolicy {
1867 pub capacity: u64,
1869 pub refill_per_minute: u64,
1871 pub window_secs: u64,
1873}
1874
1875impl BurstCreditPolicy {
1876 pub fn is_valid(&self) -> bool {
1879 self.capacity > 0 && self.refill_per_minute > 0 && self.window_secs > 0
1880 }
1881}
1882
1883#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
1885#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1886pub struct QuotaUsage {
1887 pub mem_bytes: u64,
1888 pub cpu_cores: u64,
1889 pub gpu_count: u32,
1890 pub gpu_vram_bytes: u64,
1891 pub gpu_count_by_generation: Vec<GpuGenerationQuota>,
1892 pub block_bytes: u64,
1893 pub net_bps: u64,
1894 pub tasklet_concurrency: u32,
1895 pub lease_creates_in_window: u32,
1896}
1897
1898impl QuotaUsage {
1899 pub fn usage_for_resource_kind(&self, kind: crate::ResourceKind) -> u64 {
1901 match kind {
1902 crate::ResourceKind::Mem => self.mem_bytes,
1903 crate::ResourceKind::Cpu => self.cpu_cores,
1904 crate::ResourceKind::Gpu => u64::from(self.gpu_count),
1905 crate::ResourceKind::GpuMem => self.gpu_vram_bytes,
1906 crate::ResourceKind::Block => self.block_bytes,
1907 crate::ResourceKind::Net => self.net_bps,
1908 crate::ResourceKind::Tasklet => u64::from(self.tasklet_concurrency),
1909 }
1910 }
1911
1912 pub fn gpu_usage_for_generation(&self, generation: HardwareGeneration) -> u32 {
1914 self.gpu_count_by_generation
1915 .iter()
1916 .find(|q| q.generation == generation)
1917 .map_or(0, |q| q.count)
1918 }
1919}
1920
1921#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1927#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1928#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
1929pub enum QuotaViolation {
1930 MemBytesExceeded {
1931 limit: u64,
1932 used: u64,
1933 requested: u64,
1934 },
1935 CpuCoresExceeded {
1936 limit: u64,
1937 used: u64,
1938 requested: u64,
1939 },
1940 GpuCountExceeded {
1941 generation: Option<HardwareGeneration>,
1942 limit: u32,
1943 used: u32,
1944 requested: u32,
1945 },
1946 GpuVramBytesExceeded {
1947 limit: u64,
1948 used: u64,
1949 requested: u64,
1950 },
1951 BlockBytesExceeded {
1952 limit: u64,
1953 used: u64,
1954 requested: u64,
1955 },
1956 NetBpsExceeded {
1957 limit: u64,
1958 used: u64,
1959 requested: u64,
1960 },
1961 TaskletConcurrencyExceeded {
1962 limit: u32,
1963 used: u32,
1964 requested: u32,
1965 },
1966 LeaseCreateRateExceeded {
1967 limit_per_minute: u32,
1968 used_in_window: u32,
1969 requested: u32,
1970 },
1971 ActiveLeaseCountExceeded {
1972 limit: u32,
1973 active: u32,
1974 requested: u32,
1975 },
1976 PerNodeCapacityExceeded {
1977 limit: u64,
1978 used: u64,
1979 requested: u64,
1980 },
1981 BurstCreditExhausted {
1982 capacity: u64,
1983 remaining: u64,
1984 requested: u64,
1985 },
1986 FairShareExceeded {
1987 window_secs: u64,
1988 max_share_secs: u64,
1989 used_share_secs: u64,
1990 requested_share_secs: u64,
1991 },
1992 TenantQuotaMissing,
1993}
1994
1995impl QuotaViolation {
1996 pub fn as_str(self) -> &'static str {
1998 match self {
1999 Self::MemBytesExceeded { .. } => "mem_bytes_exceeded",
2000 Self::CpuCoresExceeded { .. } => "cpu_cores_exceeded",
2001 Self::GpuCountExceeded { .. } => "gpu_count_exceeded",
2002 Self::GpuVramBytesExceeded { .. } => "gpu_vram_bytes_exceeded",
2003 Self::BlockBytesExceeded { .. } => "block_bytes_exceeded",
2004 Self::NetBpsExceeded { .. } => "net_bps_exceeded",
2005 Self::TaskletConcurrencyExceeded { .. } => "tasklet_concurrency_exceeded",
2006 Self::LeaseCreateRateExceeded { .. } => "lease_create_rate_exceeded",
2007 Self::ActiveLeaseCountExceeded { .. } => "active_lease_count_exceeded",
2008 Self::PerNodeCapacityExceeded { .. } => "per_node_capacity_exceeded",
2009 Self::BurstCreditExhausted { .. } => "burst_credit_exhausted",
2010 Self::FairShareExceeded { .. } => "fair_share_exceeded",
2011 Self::TenantQuotaMissing => "tenant_quota_missing",
2012 }
2013 }
2014
2015 pub fn human_summary(self) -> &'static str {
2017 match self {
2018 Self::MemBytesExceeded { .. } => "tenant memory quota exceeded",
2019 Self::CpuCoresExceeded { .. } => "tenant CPU quota exceeded",
2020 Self::GpuCountExceeded { .. } => "tenant GPU-count quota exceeded",
2021 Self::GpuVramBytesExceeded { .. } => "tenant GPU VRAM quota exceeded",
2022 Self::BlockBytesExceeded { .. } => "tenant block-storage quota exceeded",
2023 Self::NetBpsExceeded { .. } => "tenant network-bandwidth quota exceeded",
2024 Self::TaskletConcurrencyExceeded { .. } => "tenant tasklet-concurrency quota exceeded",
2025 Self::LeaseCreateRateExceeded { .. } => "tenant lease-create rate quota exceeded",
2026 Self::ActiveLeaseCountExceeded { .. } => "tenant active-lease count quota exceeded",
2027 Self::PerNodeCapacityExceeded { .. } => "tenant per-node capacity quota exceeded",
2028 Self::BurstCreditExhausted { .. } => "tenant burst-credit envelope exhausted",
2029 Self::FairShareExceeded { .. } => "tenant fair-share window exceeded",
2030 Self::TenantQuotaMissing => "tenant has no quota record",
2031 }
2032 }
2033
2034 pub fn rejection_reason(self) -> RejectionReason {
2037 match self {
2038 Self::BurstCreditExhausted { .. } => RejectionReason::QuotaBurstExceeded,
2039 Self::TenantQuotaMissing => RejectionReason::TenantNotFound,
2040 _ => RejectionReason::QuotaHardLimitExceeded,
2041 }
2042 }
2043}
2044
2045impl fmt::Display for QuotaViolation {
2046 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2047 f.write_str(self.as_str())
2048 }
2049}
2050
2051#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2057#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
2058#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
2059pub enum BundleAtomicity {
2060 AllOrNothing,
2065 BestEffort,
2069}
2070
2071impl BundleAtomicity {
2072 pub fn as_str(self) -> &'static str {
2074 match self {
2075 Self::AllOrNothing => "all_or_nothing",
2076 Self::BestEffort => "best_effort",
2077 }
2078 }
2079
2080 pub fn human_summary(self) -> &'static str {
2082 match self {
2083 Self::AllOrNothing => "all-or-nothing: every requirement granted or none",
2084 Self::BestEffort => "best-effort: grant as many requirements as fit",
2085 }
2086 }
2087}
2088
2089impl fmt::Display for BundleAtomicity {
2090 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2091 f.write_str(self.as_str())
2092 }
2093}
2094
2095impl Default for BundleAtomicity {
2096 fn default() -> Self {
2097 Self::AllOrNothing
2098 }
2099}
2100
2101#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2109#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
2110pub struct ResourceRequirement {
2111 pub kind: crate::ResourceKind,
2113 pub capacity: u64,
2117 pub hardware_generation: Option<HardwareGeneration>,
2123}
2124
2125#[derive(Clone, Debug, PartialEq, Eq)]
2133#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
2134pub struct ResourceBundleSpec {
2135 pub requirements: Vec<ResourceRequirement>,
2139 #[cfg_attr(feature = "serde", serde(default))]
2141 pub atomicity: BundleAtomicity,
2142}
2143
2144impl Default for ResourceBundleSpec {
2145 fn default() -> Self {
2146 Self {
2147 requirements: Vec::new(),
2148 atomicity: BundleAtomicity::AllOrNothing,
2149 }
2150 }
2151}
2152
2153#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2168#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
2169#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
2170pub enum BundleAdmissionFailure {
2171 EmptyBundle,
2175 InsufficientBundleCapacity,
2179 BundleConstraintViolation,
2185 HardwareGenerationUnavailable,
2189 OtherBundleFailure,
2195}
2196
2197impl BundleAdmissionFailure {
2198 pub fn as_str(self) -> &'static str {
2201 match self {
2202 Self::EmptyBundle => "empty_bundle",
2203 Self::InsufficientBundleCapacity => "insufficient_bundle_capacity",
2204 Self::BundleConstraintViolation => "bundle_constraint_violation",
2205 Self::HardwareGenerationUnavailable => "hardware_generation_unavailable",
2206 Self::OtherBundleFailure => "other_bundle_failure",
2207 }
2208 }
2209
2210 pub fn human_summary(self) -> &'static str {
2214 match self {
2215 Self::EmptyBundle => "bundle had zero requirements; nothing to admit",
2216 Self::InsufficientBundleCapacity => {
2217 "insufficient capacity for at least one requirement; all-or-nothing rejected"
2218 }
2219 Self::BundleConstraintViolation => {
2220 "placement constraint excludes every candidate for at least one requirement"
2221 }
2222 Self::HardwareGenerationUnavailable => {
2223 "requested hardware generation has no matching inventory in the cluster"
2224 }
2225 Self::OtherBundleFailure => "other bundle admission failure; reserved catch-all",
2226 }
2227 }
2228
2229 pub fn rejection_reason(self) -> RejectionReason {
2233 match self {
2234 Self::EmptyBundle => RejectionReason::EmptyBundle,
2235 Self::InsufficientBundleCapacity => RejectionReason::InsufficientBundleCapacity,
2236 Self::BundleConstraintViolation => RejectionReason::BundleConstraintViolation,
2237 Self::HardwareGenerationUnavailable => RejectionReason::HardwareGenerationUnavailable,
2238 Self::OtherBundleFailure => RejectionReason::OtherBundleFailure,
2239 }
2240 }
2241}
2242
2243impl fmt::Display for BundleAdmissionFailure {
2244 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2245 f.write_str(self.as_str())
2246 }
2247}
2248
2249#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2262#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
2263#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
2264pub enum BundleDecision {
2265 Approved,
2270 DeniedAllOrNothing,
2275 DeniedEmpty,
2279 PartialBestEffort,
2284}
2285
2286impl BundleDecision {
2287 pub fn as_str(self) -> &'static str {
2290 match self {
2291 Self::Approved => "approved",
2292 Self::DeniedAllOrNothing => "denied_all_or_nothing",
2293 Self::DeniedEmpty => "denied_empty",
2294 Self::PartialBestEffort => "partial_best_effort",
2295 }
2296 }
2297
2298 pub fn human_summary(self) -> &'static str {
2301 match self {
2302 Self::Approved => "bundle fully approved; reservation held pending commit",
2303 Self::DeniedAllOrNothing => {
2304 "bundle rejected; at least one requirement failed and atomicity is all-or-nothing"
2305 }
2306 Self::DeniedEmpty => "bundle rejected; zero requirements",
2307 Self::PartialBestEffort => {
2308 "best-effort bundle approved with mixed per-requirement results"
2309 }
2310 }
2311 }
2312}
2313
2314impl fmt::Display for BundleDecision {
2315 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2316 f.write_str(self.as_str())
2317 }
2318}
2319
2320#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2338#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
2339#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
2340pub enum AuditEventKind {
2341 #[cfg_attr(feature = "serde", serde(alias = "CapabilityIssued"))]
2344 CapabilityIssued,
2345 #[cfg_attr(feature = "serde", serde(alias = "CapabilityRevoked"))]
2347 CapabilityRevoked,
2348 #[cfg_attr(feature = "serde", serde(alias = "LeaseAllocated"))]
2350 LeaseAllocated,
2351 #[cfg_attr(feature = "serde", serde(alias = "LeaseRenewed"))]
2353 LeaseRenewed,
2354 #[cfg_attr(feature = "serde", serde(alias = "LeaseReleased"))]
2356 LeaseReleased,
2357 #[cfg_attr(feature = "serde", serde(alias = "LeaseExpired"))]
2360 LeaseExpired,
2361 #[cfg_attr(feature = "serde", serde(alias = "LeaseTorndown"))]
2363 LeaseTorndown,
2364 #[cfg_attr(feature = "serde", serde(alias = "LeaseFenced"))]
2366 LeaseFenced,
2367 #[cfg_attr(feature = "serde", serde(alias = "AdmissionDecided"))]
2369 AdmissionDecided,
2370 #[cfg_attr(feature = "serde", serde(alias = "Preempted"))]
2372 Preempted,
2373 #[cfg_attr(feature = "serde", serde(alias = "DrainInitiated"))]
2375 DrainInitiated,
2376 #[cfg_attr(feature = "serde", serde(alias = "ChainAnchored"))]
2380 ChainAnchored,
2381 #[cfg_attr(feature = "serde", serde(alias = "SoftModeEnabled"))]
2392 SoftModeEnabled,
2393 #[cfg_attr(feature = "serde", serde(alias = "TenantCreated"))]
2401 TenantCreated,
2402 #[cfg_attr(feature = "serde", serde(alias = "TenantDeleted"))]
2410 TenantDeleted,
2411 #[cfg_attr(feature = "serde", serde(alias = "TenantQuotaUpdated"))]
2418 TenantQuotaUpdated,
2419 #[cfg_attr(feature = "serde", serde(alias = "ProviderConformanceRecorded"))]
2426 ProviderConformanceRecorded,
2427 #[cfg_attr(feature = "serde", serde(alias = "ProviderBootstrapTokenIssued"))]
2436 ProviderBootstrapTokenIssued,
2437 #[cfg_attr(feature = "serde", serde(alias = "ProviderBootstrapExchanged"))]
2444 ProviderBootstrapExchanged,
2445 #[cfg_attr(feature = "serde", serde(alias = "ProviderCellIdentityIssued"))]
2453 ProviderCellIdentityIssued,
2454 #[cfg_attr(feature = "serde", serde(alias = "ProviderCellIdentityRotated"))]
2462 ProviderCellIdentityRotated,
2463 #[cfg_attr(feature = "serde", serde(alias = "ProviderCellIdentityRevoked"))]
2469 ProviderCellIdentityRevoked,
2470 #[cfg_attr(feature = "serde", serde(alias = "BearerTokenIssued"))]
2483 BearerTokenIssued,
2484 #[cfg_attr(feature = "serde", serde(alias = "BearerTokenRevoked"))]
2491 BearerTokenRevoked,
2492 #[cfg_attr(feature = "serde", serde(alias = "SchedulerPromoted"))]
2505 SchedulerPromoted,
2506 #[cfg_attr(feature = "serde", serde(alias = "BillingRateCardInstalled"))]
2519 BillingRateCardInstalled,
2520 #[cfg_attr(feature = "serde", serde(alias = "EdgeRewritten"))]
2532 EdgeRewritten,
2533}
2534
2535impl AuditEventKind {
2536 pub fn as_str(self) -> &'static str {
2537 match self {
2538 Self::CapabilityIssued => "capability_issued",
2539 Self::CapabilityRevoked => "capability_revoked",
2540 Self::LeaseAllocated => "lease_allocated",
2541 Self::LeaseRenewed => "lease_renewed",
2542 Self::LeaseReleased => "lease_released",
2543 Self::LeaseExpired => "lease_expired",
2544 Self::LeaseTorndown => "lease_torndown",
2545 Self::LeaseFenced => "lease_fenced",
2546 Self::AdmissionDecided => "admission_decided",
2547 Self::Preempted => "preempted",
2548 Self::DrainInitiated => "drain_initiated",
2549 Self::ChainAnchored => "chain_anchored",
2550 Self::SoftModeEnabled => "soft_mode_enabled",
2551 Self::TenantCreated => "tenant_created",
2552 Self::TenantDeleted => "tenant_deleted",
2553 Self::TenantQuotaUpdated => "tenant_quota_updated",
2554 Self::ProviderConformanceRecorded => "provider_conformance_recorded",
2555 Self::ProviderBootstrapTokenIssued => "provider_bootstrap_token_issued",
2556 Self::ProviderBootstrapExchanged => "provider_bootstrap_exchanged",
2557 Self::ProviderCellIdentityIssued => "provider_cell_identity_issued",
2558 Self::ProviderCellIdentityRotated => "provider_cell_identity_rotated",
2559 Self::ProviderCellIdentityRevoked => "provider_cell_identity_revoked",
2560 Self::BearerTokenIssued => "bearer_token_issued",
2561 Self::BearerTokenRevoked => "bearer_token_revoked",
2562 Self::SchedulerPromoted => "scheduler_promoted",
2563 Self::BillingRateCardInstalled => "billing_rate_card_installed",
2564 Self::EdgeRewritten => "edge_rewritten",
2565 }
2566 }
2567}
2568
2569impl fmt::Display for AuditEventKind {
2570 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2571 f.write_str(self.as_str())
2572 }
2573}
2574
2575#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2607#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
2608pub struct WorkloadIdentity {
2609 pub tenant: String,
2611 pub namespace_scope: Option<String>,
2614 pub service: Option<String>,
2617 pub instance_id: Option<String>,
2619 pub attestation: Option<Vec<u8>>,
2622 pub policy_gen: Option<u64>,
2626}
2627
2628impl WorkloadIdentity {
2629 pub fn system() -> Self {
2633 Self {
2634 tenant: "_system".to_string(),
2635 namespace_scope: None,
2636 service: None,
2637 instance_id: None,
2638 attestation: None,
2639 policy_gen: None,
2640 }
2641 }
2642
2643 pub fn tenant_only(tenant: impl Into<String>) -> Self {
2646 Self {
2647 tenant: tenant.into(),
2648 namespace_scope: None,
2649 service: None,
2650 instance_id: None,
2651 attestation: None,
2652 policy_gen: None,
2653 }
2654 }
2655}
2656
2657#[cfg(test)]
2662mod tests {
2663 use super::*;
2664
2665 #[test]
2666 fn preemption_reason_strings_are_stable() {
2667 assert_eq!(
2671 PreemptionReason::PriorityPreemption.as_str(),
2672 "priority_preemption"
2673 );
2674 assert_eq!(PreemptionReason::QuotaRebalance.as_str(), "quota_rebalance");
2675 assert_eq!(
2676 PreemptionReason::BurstCreditExhausted.as_str(),
2677 "burst_credit_exhausted"
2678 );
2679 assert_eq!(
2680 PreemptionReason::BudgetExhausted.as_str(),
2681 "budget_exhausted"
2682 );
2683 assert_eq!(
2684 PreemptionReason::CostCapEviction.as_str(),
2685 "cost_cap_eviction"
2686 );
2687 assert_eq!(PreemptionReason::OperatorDrain.as_str(), "operator_drain");
2688 assert_eq!(
2689 PreemptionReason::OperatorMigProfileChange.as_str(),
2690 "operator_mig_profile_change"
2691 );
2692 assert_eq!(
2693 PreemptionReason::MaintenanceWindow.as_str(),
2694 "maintenance_window"
2695 );
2696 assert_eq!(
2697 PreemptionReason::PolicyViolationRecovery.as_str(),
2698 "policy_violation_recovery"
2699 );
2700 }
2701
2702 #[test]
2709 fn preemption_reason_human_summary_is_stable() {
2710 assert_eq!(
2711 PreemptionReason::PriorityPreemption.human_summary(),
2712 "reclaimed for higher-priority work that outranked it"
2713 );
2714 assert_eq!(
2715 PreemptionReason::QuotaRebalance.human_summary(),
2716 "reclaimed because tenant fair-share moved a different tenant in"
2717 );
2718 assert_eq!(
2719 PreemptionReason::BurstCreditExhausted.human_summary(),
2720 "reclaimed because the tenant's burst-bucket envelope was exceeded"
2721 );
2722 assert_eq!(
2723 PreemptionReason::BudgetExhausted.human_summary(),
2724 "reclaimed because the tenant's spend budget was exhausted"
2725 );
2726 assert_eq!(
2727 PreemptionReason::CostCapEviction.human_summary(),
2728 "reclaimed because the global cost cap could no longer be satisfied"
2729 );
2730 assert_eq!(
2731 PreemptionReason::OperatorDrain.human_summary(),
2732 "reclaimed because an operator drained the host node"
2733 );
2734 assert_eq!(
2735 PreemptionReason::OperatorMigProfileChange.human_summary(),
2736 "reclaimed because an operator recomposed the GPU MIG profile"
2737 );
2738 assert_eq!(
2739 PreemptionReason::MaintenanceWindow.human_summary(),
2740 "reclaimed by a scheduled maintenance window"
2741 );
2742 assert_eq!(
2743 PreemptionReason::PolicyViolationRecovery.human_summary(),
2744 "reclaimed because the workload no longer satisfied a mandatory policy"
2745 );
2746 }
2747
2748 #[test]
2752 fn preemption_reason_human_summary_is_non_empty() {
2753 for reason in [
2754 PreemptionReason::PriorityPreemption,
2755 PreemptionReason::QuotaRebalance,
2756 PreemptionReason::BurstCreditExhausted,
2757 PreemptionReason::BudgetExhausted,
2758 PreemptionReason::CostCapEviction,
2759 PreemptionReason::OperatorDrain,
2760 PreemptionReason::OperatorMigProfileChange,
2761 PreemptionReason::MaintenanceWindow,
2762 PreemptionReason::PolicyViolationRecovery,
2763 ] {
2764 assert!(
2765 !reason.human_summary().is_empty(),
2766 "{reason:?}: human_summary must be non-empty"
2767 );
2768 }
2769 }
2770
2771 #[test]
2779 fn preemption_reason_human_summary_load_bearing_phrases() {
2780 assert!(
2781 PreemptionReason::OperatorDrain
2782 .human_summary()
2783 .contains("operator"),
2784 "OperatorDrain.human_summary must mention \"operator\""
2785 );
2786 assert!(
2787 PreemptionReason::OperatorMigProfileChange
2788 .human_summary()
2789 .contains("operator"),
2790 "OperatorMigProfileChange.human_summary must mention \"operator\""
2791 );
2792 assert!(
2793 PreemptionReason::OperatorMigProfileChange
2794 .human_summary()
2795 .contains("MIG"),
2796 "OperatorMigProfileChange.human_summary must mention \"MIG\""
2797 );
2798 assert!(
2799 PreemptionReason::BurstCreditExhausted
2800 .human_summary()
2801 .contains("burst"),
2802 "BurstCreditExhausted.human_summary must mention \"burst\""
2803 );
2804 assert!(
2805 PreemptionReason::BudgetExhausted
2806 .human_summary()
2807 .contains("budget"),
2808 "BudgetExhausted.human_summary must mention \"budget\""
2809 );
2810 assert!(
2811 PreemptionReason::CostCapEviction
2812 .human_summary()
2813 .contains("cost cap"),
2814 "CostCapEviction.human_summary must mention \"cost cap\""
2815 );
2816 assert!(
2817 PreemptionReason::MaintenanceWindow
2818 .human_summary()
2819 .contains("maintenance"),
2820 "MaintenanceWindow.human_summary must mention \"maintenance\""
2821 );
2822 assert!(
2823 PreemptionReason::PolicyViolationRecovery
2824 .human_summary()
2825 .contains("policy"),
2826 "PolicyViolationRecovery.human_summary must mention \"policy\""
2827 );
2828 assert!(
2829 PreemptionReason::PriorityPreemption
2830 .human_summary()
2831 .contains("higher-priority"),
2832 "PriorityPreemption.human_summary must mention \"higher-priority\""
2833 );
2834 assert!(
2835 PreemptionReason::QuotaRebalance
2836 .human_summary()
2837 .contains("fair-share"),
2838 "QuotaRebalance.human_summary must mention \"fair-share\""
2839 );
2840 }
2841
2842 #[test]
2848 fn preemption_reason_human_summary_count_and_distinct() {
2849 let summaries = [
2850 PreemptionReason::PriorityPreemption.human_summary(),
2851 PreemptionReason::QuotaRebalance.human_summary(),
2852 PreemptionReason::BurstCreditExhausted.human_summary(),
2853 PreemptionReason::BudgetExhausted.human_summary(),
2854 PreemptionReason::CostCapEviction.human_summary(),
2855 PreemptionReason::OperatorDrain.human_summary(),
2856 PreemptionReason::OperatorMigProfileChange.human_summary(),
2857 PreemptionReason::MaintenanceWindow.human_summary(),
2858 PreemptionReason::PolicyViolationRecovery.human_summary(),
2859 ];
2860 assert_eq!(summaries.len(), 9, "closed-set size pinned at 9 variants");
2861 for (i, a) in summaries.iter().enumerate() {
2862 for (j, b) in summaries.iter().enumerate() {
2863 if i != j {
2864 assert_ne!(
2865 a, b,
2866 "human_summary collision between variant {i} and {j}: {a:?}"
2867 );
2868 }
2869 }
2870 }
2871 }
2872
2873 #[test]
2881 fn preemption_reason_human_summary_differs_from_as_str() {
2882 for reason in [
2883 PreemptionReason::PriorityPreemption,
2884 PreemptionReason::QuotaRebalance,
2885 PreemptionReason::BurstCreditExhausted,
2886 PreemptionReason::BudgetExhausted,
2887 PreemptionReason::CostCapEviction,
2888 PreemptionReason::OperatorDrain,
2889 PreemptionReason::OperatorMigProfileChange,
2890 PreemptionReason::MaintenanceWindow,
2891 PreemptionReason::PolicyViolationRecovery,
2892 ] {
2893 assert_ne!(
2894 reason.as_str(),
2895 reason.human_summary(),
2896 "{reason:?}: as_str and human_summary must be different surfaces"
2897 );
2898 }
2899 }
2900
2901 #[test]
2902 fn evidence_label_ordering_matches_strength() {
2903 assert!(EvidenceLabel::DesignTarget < EvidenceLabel::LabEvidence);
2907 assert!(EvidenceLabel::LabEvidence < EvidenceLabel::StagedProviderEvidence);
2908 assert!(EvidenceLabel::StagedProviderEvidence < EvidenceLabel::DesignPartnerEvidence);
2909 assert!(EvidenceLabel::DesignPartnerEvidence < EvidenceLabel::ProductionEvidence);
2910 }
2911
2912 #[test]
2913 fn evidence_label_requires_artifact_for_anything_above_design_target() {
2914 assert!(!EvidenceLabel::DesignTarget.requires_artifact());
2915 assert!(EvidenceLabel::UnitIntegrationEvidence.requires_artifact());
2916 assert!(EvidenceLabel::LabEvidence.requires_artifact());
2917 assert!(EvidenceLabel::StagedProviderEvidence.requires_artifact());
2918 assert!(EvidenceLabel::DesignPartnerEvidence.requires_artifact());
2919 assert!(EvidenceLabel::ProductionEvidence.requires_artifact());
2920 }
2921
2922 #[test]
2923 fn audit_event_kind_strings_are_stable() {
2924 assert_eq!(
2925 AuditEventKind::CapabilityIssued.as_str(),
2926 "capability_issued"
2927 );
2928 assert_eq!(AuditEventKind::LeaseAllocated.as_str(), "lease_allocated");
2929 assert_eq!(AuditEventKind::LeaseFenced.as_str(), "lease_fenced");
2930 assert_eq!(AuditEventKind::Preempted.as_str(), "preempted");
2931 assert_eq!(AuditEventKind::ChainAnchored.as_str(), "chain_anchored");
2932 assert_eq!(
2933 AuditEventKind::SoftModeEnabled.as_str(),
2934 "soft_mode_enabled"
2935 );
2936 assert_eq!(AuditEventKind::TenantCreated.as_str(), "tenant_created");
2941 assert_eq!(AuditEventKind::TenantDeleted.as_str(), "tenant_deleted");
2942 assert_eq!(
2943 AuditEventKind::TenantQuotaUpdated.as_str(),
2944 "tenant_quota_updated"
2945 );
2946 assert_eq!(
2951 AuditEventKind::ProviderConformanceRecorded.as_str(),
2952 "provider_conformance_recorded"
2953 );
2954 assert_eq!(
2955 AuditEventKind::ProviderBootstrapTokenIssued.as_str(),
2956 "provider_bootstrap_token_issued"
2957 );
2958 assert_eq!(
2959 AuditEventKind::ProviderBootstrapExchanged.as_str(),
2960 "provider_bootstrap_exchanged"
2961 );
2962 assert_eq!(
2963 AuditEventKind::ProviderCellIdentityIssued.as_str(),
2964 "provider_cell_identity_issued"
2965 );
2966 assert_eq!(
2967 AuditEventKind::ProviderCellIdentityRotated.as_str(),
2968 "provider_cell_identity_rotated"
2969 );
2970 assert_eq!(
2971 AuditEventKind::ProviderCellIdentityRevoked.as_str(),
2972 "provider_cell_identity_revoked"
2973 );
2974 assert_eq!(
2980 AuditEventKind::BearerTokenIssued.as_str(),
2981 "bearer_token_issued"
2982 );
2983 assert_eq!(
2984 AuditEventKind::BearerTokenRevoked.as_str(),
2985 "bearer_token_revoked"
2986 );
2987 assert_eq!(
2992 AuditEventKind::SchedulerPromoted.as_str(),
2993 "scheduler_promoted"
2994 );
2995 assert_eq!(
2996 AuditEventKind::BillingRateCardInstalled.as_str(),
2997 "billing_rate_card_installed"
2998 );
2999 assert_eq!(AuditEventKind::EdgeRewritten.as_str(), "edge_rewritten");
3005 }
3006
3007 #[test]
3014 fn bearer_token_kinds_distinct_from_capability_kinds() {
3015 assert_ne!(
3016 AuditEventKind::BearerTokenIssued.as_str(),
3017 AuditEventKind::CapabilityIssued.as_str()
3018 );
3019 assert_ne!(
3020 AuditEventKind::BearerTokenRevoked.as_str(),
3021 AuditEventKind::CapabilityRevoked.as_str()
3022 );
3023 }
3024
3025 #[test]
3026 fn workload_identity_system_uses_underscore_prefix() {
3027 let id = WorkloadIdentity::system();
3032 assert!(id.tenant.starts_with('_'));
3033 assert_eq!(id.tenant, "_system");
3034 assert!(id.namespace_scope.is_none());
3035 assert!(id.service.is_none());
3036 assert!(id.instance_id.is_none());
3037 assert!(id.attestation.is_none());
3038 assert!(id.policy_gen.is_none());
3039 }
3040
3041 #[test]
3042 fn workload_identity_tenant_only_carries_tenant() {
3043 let id = WorkloadIdentity::tenant_only("acme");
3044 assert_eq!(id.tenant, "acme");
3045 assert!(id.namespace_scope.is_none());
3046 }
3047
3048 #[test]
3054 fn rejection_reason_labels_are_stable() {
3055 assert_eq!(
3056 RejectionReason::InsufficientCapacity.as_str(),
3057 "insufficient_capacity"
3058 );
3059 assert_eq!(
3060 RejectionReason::NoEligibleNodes.as_str(),
3061 "no_eligible_nodes"
3062 );
3063 assert_eq!(
3064 RejectionReason::QuotaHardLimitExceeded.as_str(),
3065 "quota_hard_limit_exceeded"
3066 );
3067 assert_eq!(
3068 RejectionReason::QuotaBurstExceeded.as_str(),
3069 "quota_burst_exceeded"
3070 );
3071 assert_eq!(
3072 RejectionReason::QuotaLeaseCountExceeded.as_str(),
3073 "quota_lease_count_exceeded"
3074 );
3075 assert_eq!(
3076 RejectionReason::QuotaPerNodeLimitExceeded.as_str(),
3077 "quota_per_node_limit_exceeded"
3078 );
3079 assert_eq!(RejectionReason::TenantNotFound.as_str(), "tenant_not_found");
3080 assert_eq!(RejectionReason::NodeFenced.as_str(), "node_fenced");
3081 assert_eq!(
3082 RejectionReason::BudgetExhausted.as_str(),
3083 "budget_exhausted"
3084 );
3085 assert_eq!(
3086 RejectionReason::ReservationExhausted.as_str(),
3087 "reservation_exhausted"
3088 );
3089 assert_eq!(
3091 RejectionReason::PlacementPolicyExcluded.as_str(),
3092 "placement_policy_excluded"
3093 );
3094 assert_eq!(
3095 RejectionReason::NodeAddressUnknown.as_str(),
3096 "node_address_unknown"
3097 );
3098 assert_eq!(RejectionReason::NodeDrained.as_str(), "node_drained");
3100 assert_eq!(RejectionReason::EmptyBundle.as_str(), "empty_bundle");
3102 assert_eq!(
3103 RejectionReason::InsufficientBundleCapacity.as_str(),
3104 "insufficient_bundle_capacity"
3105 );
3106 assert_eq!(
3107 RejectionReason::BundleConstraintViolation.as_str(),
3108 "bundle_constraint_violation"
3109 );
3110 assert_eq!(
3111 RejectionReason::HardwareGenerationUnavailable.as_str(),
3112 "hardware_generation_unavailable"
3113 );
3114 assert_eq!(
3115 RejectionReason::OtherBundleFailure.as_str(),
3116 "other_bundle_failure"
3117 );
3118 }
3119
3120 #[test]
3123 fn egress_enforcement_labels_are_stable() {
3124 assert_eq!(
3125 EgressEnforcement::FabricEnforced.as_str(),
3126 "fabric_enforced"
3127 );
3128 assert_eq!(
3129 EgressEnforcement::HostRuntimeIntegration.as_str(),
3130 "host_runtime_integration"
3131 );
3132 assert_eq!(
3133 EgressEnforcement::OperatorControlled.as_str(),
3134 "operator_controlled"
3135 );
3136 assert_eq!(EgressEnforcement::Unsupported.as_str(), "unsupported");
3137 }
3138
3139 #[test]
3141 fn egress_target_labels_are_stable() {
3142 assert_eq!(EgressTarget::WasmTasklet.as_str(), "wasm_tasklet");
3143 assert_eq!(EgressTarget::NativeProgram.as_str(), "native_program");
3144 assert_eq!(
3145 EgressTarget::ContainerAdjacent.as_str(),
3146 "container_adjacent"
3147 );
3148 assert_eq!(EgressTarget::KubernetesDra.as_str(), "kubernetes_dra");
3149 assert_eq!(EgressTarget::BareMetal.as_str(), "bare_metal");
3150 }
3151
3152 #[test]
3158 fn egress_target_to_enforcement_matrix() {
3159 assert_eq!(
3160 EgressTarget::WasmTasklet.enforcement(),
3161 EgressEnforcement::FabricEnforced,
3162 "wasm tasklet must be fabric-enforced (deny-all under wasmi)"
3163 );
3164 assert_eq!(
3165 EgressTarget::NativeProgram.enforcement(),
3166 EgressEnforcement::OperatorControlled,
3167 "native program egress is operator-controlled (kernel owns NIC)"
3168 );
3169 assert_eq!(
3170 EgressTarget::ContainerAdjacent.enforcement(),
3171 EgressEnforcement::OperatorControlled,
3172 "container-adjacent egress is operator-controlled (container runtime)"
3173 );
3174 assert_eq!(
3175 EgressTarget::KubernetesDra.enforcement(),
3176 EgressEnforcement::OperatorControlled,
3177 "Kubernetes DRA egress is operator-controlled (NetworkPolicy / CNI)"
3178 );
3179 assert_eq!(
3180 EgressTarget::BareMetal.enforcement(),
3181 EgressEnforcement::HostRuntimeIntegration,
3182 "bare-metal default is host-runtime-integration; HCL may override"
3183 );
3184 }
3185
3186 #[test]
3188 fn egress_display_matches_as_str() {
3189 assert_eq!(
3190 format!("{}", EgressEnforcement::FabricEnforced),
3191 "fabric_enforced"
3192 );
3193 assert_eq!(format!("{}", EgressTarget::WasmTasklet), "wasm_tasklet");
3194 }
3195
3196 #[test]
3199 fn economics_source_labels_are_stable() {
3200 assert_eq!(
3201 EconomicsSource::OperatorStaticConfig.as_str(),
3202 "operator_static_config"
3203 );
3204 assert_eq!(
3205 EconomicsSource::ProviderPriceSheet.as_str(),
3206 "provider_price_sheet"
3207 );
3208 assert_eq!(
3209 EconomicsSource::ProviderUsageExport.as_str(),
3210 "provider_usage_export"
3211 );
3212 assert_eq!(
3213 EconomicsSource::MeasuredPowerTelemetry.as_str(),
3214 "measured_power_telemetry"
3215 );
3216 assert_eq!(
3217 EconomicsSource::ThirdPartyCarbonFeed.as_str(),
3218 "third_party_carbon_feed"
3219 );
3220 assert_eq!(
3221 EconomicsSource::DerivedEstimate.as_str(),
3222 "derived_estimate"
3223 );
3224 }
3225
3226 #[test]
3228 fn observation_confidence_labels_are_stable() {
3229 assert_eq!(ObservationConfidence::Low.as_str(), "low");
3230 assert_eq!(ObservationConfidence::Medium.as_str(), "medium");
3231 assert_eq!(ObservationConfidence::High.as_str(), "high");
3232 }
3233
3234 #[test]
3238 fn observation_confidence_is_ordinal() {
3239 assert!(ObservationConfidence::Low < ObservationConfidence::Medium);
3240 assert!(ObservationConfidence::Medium < ObservationConfidence::High);
3241 assert!(ObservationConfidence::Low < ObservationConfidence::High);
3242 let floor = ObservationConfidence::Medium;
3244 let high = ObservationConfidence::High;
3245 let low = ObservationConfidence::Low;
3246 assert!(high >= floor);
3247 assert!(!(low >= floor));
3248 }
3249
3250 #[test]
3253 fn economics_generation_is_monotonic() {
3254 assert_eq!(EconomicsGeneration::UNANCHORED.0, 0);
3255 let g1 = EconomicsGeneration::UNANCHORED.next();
3256 assert_eq!(g1.0, 1);
3257 let g2 = g1.next();
3258 assert_eq!(g2.0, 2);
3259 assert!(g2 > g1);
3260 assert!(g1 > EconomicsGeneration::UNANCHORED);
3261 let max = EconomicsGeneration(u64::MAX);
3263 assert_eq!(max.next().0, u64::MAX);
3264 }
3265
3266 #[test]
3269 fn economics_display_matches_as_str() {
3270 assert_eq!(
3271 format!("{}", EconomicsSource::OperatorStaticConfig),
3272 "operator_static_config"
3273 );
3274 assert_eq!(format!("{}", ObservationConfidence::High), "high");
3275 assert_eq!(format!("{}", EconomicsGeneration(42)), "gen=42");
3276 }
3277
3278 #[test]
3283 fn economics_observation_freshness_boundaries() {
3284 let obs = EconomicsObservation {
3285 value: 42u32,
3286 source: EconomicsSource::OperatorStaticConfig,
3287 confidence: ObservationConfidence::Medium,
3288 observed_at: 100,
3289 valid_until: 200,
3290 generation: EconomicsGeneration(1),
3291 };
3292 assert!(!obs.is_fresh(99));
3294 assert!(obs.is_fresh(100));
3296 assert!(obs.is_fresh(150));
3298 assert!(!obs.is_fresh(200));
3300 assert!(!obs.is_fresh(201));
3302 }
3303
3304 #[test]
3308 fn economics_observation_confidence_floor() {
3309 let high = EconomicsObservation {
3310 value: 1u32,
3311 source: EconomicsSource::MeasuredPowerTelemetry,
3312 confidence: ObservationConfidence::High,
3313 observed_at: 0,
3314 valid_until: u64::MAX,
3315 generation: EconomicsGeneration(1),
3316 };
3317 let low = EconomicsObservation {
3318 confidence: ObservationConfidence::Low,
3319 ..high.clone()
3320 };
3321 let medium = EconomicsObservation {
3322 confidence: ObservationConfidence::Medium,
3323 ..high.clone()
3324 };
3325
3326 assert!(high.meets_floor(ObservationConfidence::Low));
3327 assert!(high.meets_floor(ObservationConfidence::Medium));
3328 assert!(high.meets_floor(ObservationConfidence::High));
3329
3330 assert!(medium.meets_floor(ObservationConfidence::Low));
3331 assert!(medium.meets_floor(ObservationConfidence::Medium));
3332 assert!(!medium.meets_floor(ObservationConfidence::High));
3333
3334 assert!(low.meets_floor(ObservationConfidence::Low));
3335 assert!(!low.meets_floor(ObservationConfidence::Medium));
3336 assert!(!low.meets_floor(ObservationConfidence::High));
3337 }
3338
3339 #[test]
3344 fn economics_observation_admissible_combines_freshness_and_confidence() {
3345 let obs = EconomicsObservation {
3346 value: 1u32,
3347 source: EconomicsSource::ProviderUsageExport,
3348 confidence: ObservationConfidence::Medium,
3349 observed_at: 100,
3350 valid_until: 200,
3351 generation: EconomicsGeneration(7),
3352 };
3353 assert!(obs.admissible(150, ObservationConfidence::Medium));
3355 assert!(obs.admissible(150, ObservationConfidence::Low));
3356 assert!(!obs.admissible(150, ObservationConfidence::High));
3358 assert!(!obs.admissible(200, ObservationConfidence::Medium));
3360 assert!(!obs.admissible(201, ObservationConfidence::Medium));
3361 assert!(!obs.admissible(200, ObservationConfidence::High));
3363 }
3364
3365 #[test]
3370 fn economics_observation_is_generic_over_value_type() {
3371 #[derive(Clone, Debug, PartialEq, Eq)]
3372 struct CarbonGrams {
3373 grams_co2e_per_kwh: u64,
3374 }
3375 let obs = EconomicsObservation {
3376 value: CarbonGrams {
3377 grams_co2e_per_kwh: 250,
3378 },
3379 source: EconomicsSource::ThirdPartyCarbonFeed,
3380 confidence: ObservationConfidence::Medium,
3381 observed_at: 1_700_000_000,
3382 valid_until: 1_700_003_600,
3383 generation: EconomicsGeneration(1),
3384 };
3385 assert_eq!(obs.value.grams_co2e_per_kwh, 250);
3386 assert!(obs.admissible(1_700_001_000, ObservationConfidence::Medium));
3387 }
3388
3389 #[test]
3391 fn rejection_reason_display_matches_as_str() {
3392 assert_eq!(
3393 format!("{}", RejectionReason::InsufficientCapacity),
3394 "insufficient_capacity"
3395 );
3396 assert_eq!(format!("{}", RejectionReason::NodeDrained), "node_drained");
3398 }
3399
3400 #[test]
3407 fn rejection_reason_human_summary_is_stable() {
3408 assert_eq!(
3409 RejectionReason::InsufficientCapacity.human_summary(),
3410 "insufficient free capacity across all eligible nodes"
3411 );
3412 assert_eq!(
3413 RejectionReason::NoEligibleNodes.human_summary(),
3414 "no nodes match the placement constraint"
3415 );
3416 assert_eq!(
3417 RejectionReason::QuotaHardLimitExceeded.human_summary(),
3418 "tenant quota hard limit reached"
3419 );
3420 assert_eq!(
3421 RejectionReason::QuotaBurstExceeded.human_summary(),
3422 "tenant burst quota cap reached"
3423 );
3424 assert_eq!(
3425 RejectionReason::QuotaLeaseCountExceeded.human_summary(),
3426 "tenant lease count limit reached"
3427 );
3428 assert_eq!(
3429 RejectionReason::QuotaPerNodeLimitExceeded.human_summary(),
3430 "tenant per-node capacity limit reached"
3431 );
3432 assert_eq!(
3433 RejectionReason::TenantNotFound.human_summary(),
3434 "tenant id not registered with the scheduler"
3435 );
3436 assert_eq!(
3437 RejectionReason::NodeFenced.human_summary(),
3438 "selected node is fenced for the requested resource type"
3439 );
3440 assert_eq!(
3441 RejectionReason::BudgetExhausted.human_summary(),
3442 "power or cost budget cap reached"
3443 );
3444 assert_eq!(
3445 RejectionReason::ReservationExhausted.human_summary(),
3446 "reservation has insufficient remaining capacity"
3447 );
3448 assert_eq!(
3449 RejectionReason::PlacementPolicyExcluded.human_summary(),
3450 "placement policy excluded this candidate"
3451 );
3452 assert_eq!(
3453 RejectionReason::NodeAddressUnknown.human_summary(),
3454 "scheduler does not have a control-plane address for this node"
3455 );
3456 assert_eq!(
3457 RejectionReason::NodeDrained.human_summary(),
3458 "target node is drained for maintenance; admission resumes after the node returns to accepting mode"
3459 );
3460 assert_eq!(
3461 RejectionReason::EmptyBundle.human_summary(),
3462 "bundle had zero requirements; nothing to admit"
3463 );
3464 assert_eq!(
3465 RejectionReason::InsufficientBundleCapacity.human_summary(),
3466 "insufficient capacity for at least one bundle requirement"
3467 );
3468 assert_eq!(
3469 RejectionReason::BundleConstraintViolation.human_summary(),
3470 "bundle placement constraint excludes every viable candidate plan"
3471 );
3472 assert_eq!(
3473 RejectionReason::HardwareGenerationUnavailable.human_summary(),
3474 "requested bundle hardware generation has no matching inventory"
3475 );
3476 assert_eq!(
3477 RejectionReason::OtherBundleFailure.human_summary(),
3478 "other bundle admission failure"
3479 );
3480 }
3481
3482 #[test]
3485 fn fair_share_window_valid_accepts_well_formed() {
3486 let w = FairShareWindow {
3487 tenant_id: 0x0000_0000_0000_0000_0000_0000_0000_00ac,
3488 priority_class_weight: 100,
3489 window_secs: 600,
3490 min_share_secs: 60,
3491 max_share_secs: 300,
3492 generation: EconomicsGeneration::UNANCHORED.next(),
3493 };
3494 assert!(w.is_valid());
3495 }
3496
3497 #[test]
3501 fn fair_share_window_invalid_rejects_min_above_max() {
3502 let w = FairShareWindow {
3503 tenant_id: 1,
3504 priority_class_weight: 50,
3505 window_secs: 600,
3506 min_share_secs: 400,
3507 max_share_secs: 300,
3508 generation: EconomicsGeneration::UNANCHORED,
3509 };
3510 assert!(!w.is_valid());
3511 }
3512
3513 #[test]
3516 fn fair_share_window_invalid_rejects_zero_weight() {
3517 let w = FairShareWindow {
3518 tenant_id: 1,
3519 priority_class_weight: 0,
3520 window_secs: 600,
3521 min_share_secs: 60,
3522 max_share_secs: 300,
3523 generation: EconomicsGeneration::UNANCHORED,
3524 };
3525 assert!(!w.is_valid());
3526 }
3527
3528 #[test]
3531 fn fair_share_window_invalid_rejects_zero_window() {
3532 let w = FairShareWindow {
3533 tenant_id: 1,
3534 priority_class_weight: 100,
3535 window_secs: 0,
3536 min_share_secs: 0,
3537 max_share_secs: 0,
3538 generation: EconomicsGeneration::UNANCHORED,
3539 };
3540 assert!(!w.is_valid());
3541 }
3542
3543 #[cfg(feature = "serde")]
3547 #[test]
3548 fn fair_share_window_serde_round_trip() {
3549 let w = FairShareWindow {
3550 tenant_id: 0xdead_beef_cafe_f00d_1234_5678_9abc_def0,
3551 priority_class_weight: 250,
3552 window_secs: 3600,
3553 min_share_secs: 120,
3554 max_share_secs: 1800,
3555 generation: EconomicsGeneration(7),
3556 };
3557 let json = serde_json::to_string(&w).expect("serialize");
3558 let back: FairShareWindow = serde_json::from_str(&json).expect("deserialize");
3559 assert_eq!(w, back);
3560 }
3561
3562 #[test]
3566 fn fair_share_scope_kind_labels_are_stable() {
3567 assert_eq!(FairShareScope::Tenant { tenant_id: 7 }.kind(), "tenant");
3568 assert_eq!(FairShareScope::Project { project_id: 11 }.kind(), "project");
3569 assert_eq!(
3570 FairShareScope::PriorityClass {
3571 priority: Priority::Guaranteed
3572 }
3573 .kind(),
3574 "priority_class"
3575 );
3576 }
3577
3578 #[test]
3581 fn weighted_fair_share_policy_accepts_tenant_project_and_priority_scopes() {
3582 let policy = WeightedFairSharePolicy {
3583 window_secs: 3600,
3584 generation: EconomicsGeneration(9),
3585 entries: vec![
3586 FairShareWeight {
3587 scope: FairShareScope::Tenant { tenant_id: 1 },
3588 weight: 100,
3589 min_share_secs: 60,
3590 max_share_secs: 1800,
3591 },
3592 FairShareWeight {
3593 scope: FairShareScope::Project { project_id: 22 },
3594 weight: 50,
3595 min_share_secs: 0,
3596 max_share_secs: 900,
3597 },
3598 FairShareWeight {
3599 scope: FairShareScope::PriorityClass {
3600 priority: Priority::Scavenger,
3601 },
3602 weight: 10,
3603 min_share_secs: 0,
3604 max_share_secs: 300,
3605 },
3606 ],
3607 };
3608
3609 assert!(policy.is_valid());
3610 assert_eq!(policy.total_weight(), 160);
3611 }
3612
3613 #[test]
3616 fn weighted_fair_share_policy_rejects_duplicate_scope() {
3617 let scope = FairShareScope::Tenant { tenant_id: 1 };
3618 let policy = WeightedFairSharePolicy {
3619 window_secs: 3600,
3620 generation: EconomicsGeneration(9),
3621 entries: vec![
3622 FairShareWeight {
3623 scope,
3624 weight: 100,
3625 min_share_secs: 0,
3626 max_share_secs: 100,
3627 },
3628 FairShareWeight {
3629 scope,
3630 weight: 200,
3631 min_share_secs: 0,
3632 max_share_secs: 200,
3633 },
3634 ],
3635 };
3636
3637 assert!(!policy.is_valid());
3638 }
3639
3640 #[test]
3643 fn weighted_fair_share_policy_rejects_zero_window_zero_weight_and_bad_bounds() {
3644 let zero_window = WeightedFairSharePolicy {
3645 window_secs: 0,
3646 generation: EconomicsGeneration(1),
3647 entries: vec![FairShareWeight {
3648 scope: FairShareScope::Tenant { tenant_id: 1 },
3649 weight: 100,
3650 min_share_secs: 0,
3651 max_share_secs: 100,
3652 }],
3653 };
3654 assert!(!zero_window.is_valid());
3655
3656 let zero_weight = WeightedFairSharePolicy {
3657 window_secs: 60,
3658 generation: EconomicsGeneration(1),
3659 entries: vec![FairShareWeight {
3660 scope: FairShareScope::Tenant { tenant_id: 1 },
3661 weight: 0,
3662 min_share_secs: 0,
3663 max_share_secs: 100,
3664 }],
3665 };
3666 assert!(!zero_weight.is_valid());
3667
3668 let bad_bounds = WeightedFairSharePolicy {
3669 window_secs: 60,
3670 generation: EconomicsGeneration(1),
3671 entries: vec![FairShareWeight {
3672 scope: FairShareScope::Tenant { tenant_id: 1 },
3673 weight: 100,
3674 min_share_secs: 101,
3675 max_share_secs: 100,
3676 }],
3677 };
3678 assert!(!bad_bounds.is_valid());
3679 }
3680
3681 #[cfg(feature = "serde")]
3684 #[test]
3685 fn weighted_fair_share_policy_serde_round_trip() {
3686 let policy = WeightedFairSharePolicy {
3687 window_secs: 300,
3688 generation: EconomicsGeneration(12),
3689 entries: vec![
3690 FairShareWeight {
3691 scope: FairShareScope::Tenant { tenant_id: 99 },
3692 weight: 100,
3693 min_share_secs: 10,
3694 max_share_secs: 200,
3695 },
3696 FairShareWeight {
3697 scope: FairShareScope::PriorityClass {
3698 priority: Priority::Guaranteed,
3699 },
3700 weight: 300,
3701 min_share_secs: 100,
3702 max_share_secs: 300,
3703 },
3704 ],
3705 };
3706
3707 let json = serde_json::to_string(&policy).expect("serialize");
3708 let back: WeightedFairSharePolicy =
3709 serde_json::from_str(&json).expect("deserialize fair-share policy");
3710 assert_eq!(policy, back);
3711 }
3712
3713 #[test]
3718 fn priority_as_str_is_stable() {
3719 assert_eq!(Priority::Scavenger.as_str(), "scavenger");
3720 assert_eq!(Priority::Standard.as_str(), "standard");
3721 assert_eq!(Priority::Guaranteed.as_str(), "guaranteed");
3722 }
3723
3724 #[test]
3730 fn priority_human_summary_is_stable() {
3731 assert_eq!(
3732 Priority::Scavenger.human_summary(),
3733 "uses leftover capacity; preempted first when contention rises"
3734 );
3735 assert_eq!(
3736 Priority::Standard.human_summary(),
3737 "uses unreserved capacity; preemptible by guaranteed work"
3738 );
3739 assert_eq!(
3740 Priority::Guaranteed.human_summary(),
3741 "reserved capacity; never preempted by ordinary scheduling"
3742 );
3743 }
3744
3745 #[test]
3751 fn priority_human_summary_load_bearing_phrases() {
3752 assert!(
3753 Priority::Scavenger.human_summary().contains("preempt"),
3754 "Scavenger summary should mention preemption"
3755 );
3756 assert!(
3757 Priority::Standard.human_summary().contains("preempt"),
3758 "Standard summary should mention preemption"
3759 );
3760 assert!(
3761 Priority::Guaranteed.human_summary().contains("never"),
3762 "Guaranteed summary should mention 'never' (no preemption)"
3763 );
3764 }
3765
3766 #[test]
3771 fn priority_count_and_distinct() {
3772 let all = [
3773 Priority::Scavenger,
3774 Priority::Standard,
3775 Priority::Guaranteed,
3776 ];
3777 assert_eq!(all.len(), 3, "Priority must have exactly 3 variants");
3778
3779 let labels: Vec<&str> = all.iter().map(|p| p.as_str()).collect();
3781 let mut sorted = labels.clone();
3782 sorted.sort_unstable();
3783 sorted.dedup();
3784 assert_eq!(sorted.len(), labels.len(), "as_str labels must be distinct");
3785
3786 let summaries: Vec<&str> = all.iter().map(|p| p.human_summary()).collect();
3788 let mut sorted_s = summaries.clone();
3789 sorted_s.sort_unstable();
3790 sorted_s.dedup();
3791 assert_eq!(
3792 sorted_s.len(),
3793 summaries.len(),
3794 "human_summary strings must be distinct"
3795 );
3796 }
3797
3798 #[test]
3804 fn priority_ordering_is_stable() {
3805 assert!(Priority::Guaranteed > Priority::Standard);
3806 assert!(Priority::Standard > Priority::Scavenger);
3807 assert!(Priority::Guaranteed > Priority::Scavenger);
3808
3809 let mut prios = [
3810 Priority::Standard,
3811 Priority::Guaranteed,
3812 Priority::Scavenger,
3813 ];
3814 prios.sort();
3815 assert_eq!(
3816 prios,
3817 [
3818 Priority::Scavenger,
3819 Priority::Standard,
3820 Priority::Guaranteed
3821 ]
3822 );
3823 }
3824
3825 #[test]
3831 fn priority_human_summary_differs_from_as_str() {
3832 for p in [
3833 Priority::Scavenger,
3834 Priority::Standard,
3835 Priority::Guaranteed,
3836 ] {
3837 assert_ne!(
3838 p.as_str(),
3839 p.human_summary(),
3840 "{p:?}: as_str and human_summary must be different surfaces"
3841 );
3842 }
3843 }
3844
3845 #[test]
3848 fn priority_display_matches_as_str() {
3849 assert_eq!(format!("{}", Priority::Scavenger), "scavenger");
3850 assert_eq!(format!("{}", Priority::Standard), "standard");
3851 assert_eq!(format!("{}", Priority::Guaranteed), "guaranteed");
3852 }
3853
3854 #[test]
3862 fn priority_from_str_canonical_labels() {
3863 use core::str::FromStr;
3864 for (label, expected) in [
3865 ("scavenger", Priority::Scavenger),
3866 ("standard", Priority::Standard),
3867 ("guaranteed", Priority::Guaranteed),
3868 ] {
3869 let got = Priority::from_str(label)
3870 .unwrap_or_else(|e| panic!("from_str({label}) failed for canonical label: {e}"));
3871 assert_eq!(got, expected, "label {label} mapped to wrong variant");
3872 assert_eq!(
3873 got.as_str(),
3874 label,
3875 "round-trip from {label} must yield {label}"
3876 );
3877 }
3878 }
3879
3880 #[test]
3890 fn priority_from_str_accepts_best_effort_deprecation_alias() {
3891 use core::str::FromStr;
3892 assert_eq!(
3893 Priority::from_str("best_effort").expect("alias must parse"),
3894 Priority::Standard,
3895 "best_effort is the deprecation alias for Standard"
3896 );
3897 }
3898
3899 #[test]
3909 fn priority_from_str_unknown_label_fails_closed() {
3910 use core::str::FromStr;
3911 let err = Priority::from_str("bogus").expect_err("unknown label must fail");
3912 assert_eq!(err.0, "bogus", "error must carry the offending input");
3913 let msg = format!("{err}");
3914 assert!(
3915 msg.contains("bogus"),
3916 "error message must echo the offending input: {msg}"
3917 );
3918 assert!(
3919 msg.contains("scavenger") && msg.contains("standard") && msg.contains("guaranteed"),
3920 "error message must list the canonical set: {msg}"
3921 );
3922 }
3923
3924 #[test]
3931 fn priority_from_str_does_not_loose_match_alias_variants() {
3932 use core::str::FromStr;
3933 for bad in [
3934 "besteffort",
3935 "Best Effort",
3936 "BEST_EFFORT",
3937 "",
3938 "Standard",
3939 "STANDARD",
3940 ] {
3941 assert!(
3942 Priority::from_str(bad).is_err(),
3943 "from_str must reject loose-match {bad:?}"
3944 );
3945 }
3946 }
3947
3948 #[test]
3957 fn rejection_reason_human_summary_differs_from_as_str() {
3958 for reason in [
3959 RejectionReason::InsufficientCapacity,
3960 RejectionReason::NoEligibleNodes,
3961 RejectionReason::QuotaHardLimitExceeded,
3962 RejectionReason::QuotaBurstExceeded,
3963 RejectionReason::QuotaLeaseCountExceeded,
3964 RejectionReason::QuotaPerNodeLimitExceeded,
3965 RejectionReason::TenantNotFound,
3966 RejectionReason::NodeFenced,
3967 RejectionReason::BudgetExhausted,
3968 RejectionReason::ReservationExhausted,
3969 RejectionReason::PlacementPolicyExcluded,
3970 RejectionReason::NodeAddressUnknown,
3971 RejectionReason::NodeDrained,
3972 RejectionReason::EmptyBundle,
3973 RejectionReason::InsufficientBundleCapacity,
3974 RejectionReason::BundleConstraintViolation,
3975 RejectionReason::HardwareGenerationUnavailable,
3976 RejectionReason::OtherBundleFailure,
3977 ] {
3978 assert_ne!(
3979 reason.as_str(),
3980 reason.human_summary(),
3981 "{reason:?}: as_str and human_summary must be different surfaces"
3982 );
3983 }
3984 }
3985
3986 #[test]
3996 fn revoke_state_as_str_is_stable() {
3997 assert_eq!(RevokeState::Active.as_str(), "active");
3998 assert_eq!(RevokeState::RevokeWarning.as_str(), "revoke_warning");
3999 assert_eq!(RevokeState::GraceRunning.as_str(), "grace_running");
4000 assert_eq!(
4001 RevokeState::CheckpointReported.as_str(),
4002 "checkpoint_reported"
4003 );
4004 assert_eq!(RevokeState::ForcedTeardown.as_str(), "forced_teardown");
4005 assert_eq!(RevokeState::Torndown.as_str(), "torndown");
4006 assert_eq!(RevokeState::Expired.as_str(), "expired");
4007 assert_eq!(RevokeState::Fenced.as_str(), "fenced");
4008 assert_eq!(RevokeState::FailedClosed.as_str(), "failed_closed");
4009 }
4010
4011 #[test]
4016 fn revoke_state_human_summary_is_stable() {
4017 assert_eq!(
4018 RevokeState::Active.human_summary(),
4019 "lease is active; no revoke pending"
4020 );
4021 assert_eq!(
4022 RevokeState::RevokeWarning.human_summary(),
4023 "revoke initiated; workload has been notified"
4024 );
4025 assert_eq!(
4026 RevokeState::GraceRunning.human_summary(),
4027 "grace period running; workload may checkpoint"
4028 );
4029 assert_eq!(
4030 RevokeState::CheckpointReported.human_summary(),
4031 "workload reported checkpoint; cooperative teardown"
4032 );
4033 assert_eq!(
4034 RevokeState::ForcedTeardown.human_summary(),
4035 "forced teardown in progress; no grace or grace exceeded"
4036 );
4037 assert_eq!(
4038 RevokeState::Torndown.human_summary(),
4039 "teardown complete; lease released"
4040 );
4041 assert_eq!(
4042 RevokeState::Expired.human_summary(),
4043 "lease TTL aged out without revoke"
4044 );
4045 assert_eq!(
4046 RevokeState::Fenced.human_summary(),
4047 "teardown failed; resource fenced for forensic clearing"
4048 );
4049 assert_eq!(
4050 RevokeState::FailedClosed.human_summary(),
4051 "fail-closed terminal; lease invariant violated"
4052 );
4053 }
4054
4055 #[test]
4060 fn revoke_state_human_summary_load_bearing_phrases() {
4061 assert!(
4062 RevokeState::Active.human_summary().contains("active"),
4063 "Active summary should mention 'active'"
4064 );
4065 assert!(
4066 RevokeState::RevokeWarning
4067 .human_summary()
4068 .contains("revoke"),
4069 "RevokeWarning summary should mention 'revoke'"
4070 );
4071 assert!(
4072 RevokeState::GraceRunning.human_summary().contains("grace"),
4073 "GraceRunning summary should mention 'grace'"
4074 );
4075 assert!(
4076 RevokeState::CheckpointReported
4077 .human_summary()
4078 .contains("checkpoint"),
4079 "CheckpointReported summary should mention 'checkpoint'"
4080 );
4081 assert!(
4082 RevokeState::ForcedTeardown
4083 .human_summary()
4084 .contains("forced"),
4085 "ForcedTeardown summary should mention 'forced'"
4086 );
4087 assert!(
4088 RevokeState::Torndown.human_summary().contains("teardown"),
4089 "Torndown summary should mention 'teardown'"
4090 );
4091 assert!(
4092 RevokeState::Expired.human_summary().contains("TTL"),
4093 "Expired summary should mention 'TTL'"
4094 );
4095 assert!(
4096 RevokeState::Fenced.human_summary().contains("fenced"),
4097 "Fenced summary should mention 'fenced'"
4098 );
4099 assert!(
4100 RevokeState::FailedClosed
4101 .human_summary()
4102 .contains("invariant"),
4103 "FailedClosed summary should mention 'invariant'"
4104 );
4105 }
4106
4107 #[test]
4116 fn revoke_state_count_and_distinct() {
4117 let all = [
4118 RevokeState::Active,
4119 RevokeState::RevokeWarning,
4120 RevokeState::GraceRunning,
4121 RevokeState::CheckpointReported,
4122 RevokeState::ForcedTeardown,
4123 RevokeState::Torndown,
4124 RevokeState::Expired,
4125 RevokeState::Fenced,
4126 RevokeState::FailedClosed,
4127 ];
4128 assert_eq!(all.len(), 9, "RevokeState must have exactly 9 variants");
4129
4130 let labels: Vec<&str> = all.iter().map(|s| s.as_str()).collect();
4132 let mut sorted = labels.clone();
4133 sorted.sort_unstable();
4134 sorted.dedup();
4135 assert_eq!(sorted.len(), labels.len(), "as_str labels must be distinct");
4136
4137 let summaries: Vec<&str> = all.iter().map(|s| s.human_summary()).collect();
4139 let mut sorted_s = summaries.clone();
4140 sorted_s.sort_unstable();
4141 sorted_s.dedup();
4142 assert_eq!(
4143 sorted_s.len(),
4144 summaries.len(),
4145 "human_summary strings must be distinct"
4146 );
4147
4148 let mut combined: Vec<&str> = labels
4152 .iter()
4153 .copied()
4154 .chain(summaries.iter().copied())
4155 .collect();
4156 combined.sort_unstable();
4157 let before = combined.len();
4158 combined.dedup();
4159 assert_eq!(
4160 combined.len(),
4161 before,
4162 "as_str and human_summary surfaces must not share any string"
4163 );
4164 }
4165
4166 #[test]
4173 fn revoke_state_terminal_set() {
4174 assert!(RevokeState::Torndown.is_terminal());
4176 assert!(RevokeState::Fenced.is_terminal());
4177 assert!(RevokeState::FailedClosed.is_terminal());
4178 assert!(RevokeState::Expired.is_terminal());
4179
4180 assert!(!RevokeState::Active.is_terminal());
4182 assert!(!RevokeState::RevokeWarning.is_terminal());
4183 assert!(!RevokeState::GraceRunning.is_terminal());
4184 assert!(!RevokeState::CheckpointReported.is_terminal());
4185 assert!(!RevokeState::ForcedTeardown.is_terminal());
4186 }
4187
4188 #[test]
4194 fn revoke_state_legal_transitions() {
4195 use RevokeState::*;
4196
4197 assert!(Active.legal_transition_to(RevokeWarning));
4199 assert!(Active.legal_transition_to(ForcedTeardown));
4200 assert!(Active.legal_transition_to(Expired));
4201 assert!(Active.legal_transition_to(Fenced));
4202
4203 assert!(RevokeWarning.legal_transition_to(GraceRunning));
4205 assert!(GraceRunning.legal_transition_to(CheckpointReported));
4206 assert!(CheckpointReported.legal_transition_to(Torndown));
4207 assert!(ForcedTeardown.legal_transition_to(Torndown));
4208 assert!(Expired.legal_transition_to(FailedClosed));
4209
4210 assert!(CheckpointReported.legal_transition_to(Fenced));
4212 assert!(ForcedTeardown.legal_transition_to(Fenced));
4213
4214 assert!(Active.legal_transition_to(FailedClosed));
4216 assert!(RevokeWarning.legal_transition_to(FailedClosed));
4217 assert!(GraceRunning.legal_transition_to(FailedClosed));
4218 assert!(CheckpointReported.legal_transition_to(FailedClosed));
4219 assert!(ForcedTeardown.legal_transition_to(FailedClosed));
4220
4221 assert!(!Torndown.legal_transition_to(Active));
4223 assert!(!Torndown.legal_transition_to(RevokeWarning));
4224 assert!(!Fenced.legal_transition_to(Active));
4225 assert!(!FailedClosed.legal_transition_to(Active));
4226
4227 assert!(!RevokeWarning.legal_transition_to(Active));
4230 assert!(!GraceRunning.legal_transition_to(Active));
4231 assert!(!GraceRunning.legal_transition_to(RevokeWarning));
4232 assert!(!CheckpointReported.legal_transition_to(GraceRunning));
4233 assert!(!CheckpointReported.legal_transition_to(ForcedTeardown));
4234
4235 assert!(!Active.legal_transition_to(GraceRunning));
4239 assert!(!Active.legal_transition_to(CheckpointReported));
4240 assert!(!Active.legal_transition_to(Torndown));
4241
4242 assert!(!Expired.legal_transition_to(Torndown));
4245 assert!(!Expired.legal_transition_to(Fenced));
4246 assert!(!Expired.legal_transition_to(Active));
4247 }
4248
4249 #[test]
4255 fn revoke_state_human_summary_differs_from_as_str() {
4256 for state in [
4257 RevokeState::Active,
4258 RevokeState::RevokeWarning,
4259 RevokeState::GraceRunning,
4260 RevokeState::CheckpointReported,
4261 RevokeState::ForcedTeardown,
4262 RevokeState::Torndown,
4263 RevokeState::Expired,
4264 RevokeState::Fenced,
4265 RevokeState::FailedClosed,
4266 ] {
4267 assert_ne!(
4268 state.as_str(),
4269 state.human_summary(),
4270 "{state:?}: as_str and human_summary must be different surfaces"
4271 );
4272 }
4273 }
4274
4275 #[test]
4278 fn revoke_state_display_matches_as_str() {
4279 assert_eq!(format!("{}", RevokeState::Active), "active");
4280 assert_eq!(format!("{}", RevokeState::RevokeWarning), "revoke_warning");
4281 assert_eq!(format!("{}", RevokeState::GraceRunning), "grace_running");
4282 assert_eq!(
4283 format!("{}", RevokeState::CheckpointReported),
4284 "checkpoint_reported"
4285 );
4286 assert_eq!(
4287 format!("{}", RevokeState::ForcedTeardown),
4288 "forced_teardown"
4289 );
4290 assert_eq!(format!("{}", RevokeState::Torndown), "torndown");
4291 assert_eq!(format!("{}", RevokeState::Expired), "expired");
4292 assert_eq!(format!("{}", RevokeState::Fenced), "fenced");
4293 assert_eq!(format!("{}", RevokeState::FailedClosed), "failed_closed");
4294 }
4295
4296 #[test]
4301 fn slice_122_hard_revoke_policy_is_default_and_has_no_deadline() {
4302 let policy = RevokeGracePolicy::default();
4303 assert_eq!(policy, RevokeGracePolicy::hard_revoke());
4304 assert!(policy.is_hard_revoke());
4305 assert!(!policy.checkpoint_hooks_allowed());
4306 assert_eq!(policy.grace_deadline_unix_secs(1_700_000_000), Ok(None));
4307 }
4308
4309 #[test]
4310 fn slice_122_bounded_grace_policy_accepts_checkpoint_hooks_with_deadline() {
4311 let policy = RevokeGracePolicy::bounded(30, 120, true).unwrap();
4312 assert!(!policy.is_hard_revoke());
4313 assert!(policy.checkpoint_hooks_allowed());
4314 assert_eq!(
4315 policy.grace_deadline_unix_secs(1_700_000_000),
4316 Ok(Some(1_700_000_030))
4317 );
4318 }
4319
4320 #[test]
4321 fn slice_122_grace_policy_rejects_grace_above_maximum() {
4322 assert_eq!(
4323 RevokeGracePolicy::bounded(121, 120, false),
4324 Err(RevokeGracePolicyError::GraceExceedsMaximum)
4325 );
4326 }
4327
4328 #[test]
4329 fn slice_122_grace_policy_rejects_checkpoint_hooks_without_grace() {
4330 assert_eq!(
4331 RevokeGracePolicy::bounded(0, 0, true),
4332 Err(RevokeGracePolicyError::CheckpointHooksRequireGrace)
4333 );
4334 }
4335
4336 #[test]
4337 fn slice_122_grace_deadline_overflow_fails_closed() {
4338 let policy = RevokeGracePolicy::bounded(30, 30, false).unwrap();
4339 assert_eq!(
4340 policy.grace_deadline_unix_secs(u64::MAX - 10),
4341 Err(RevokeGracePolicyError::DeadlineOverflow)
4342 );
4343 }
4344
4345 #[test]
4346 fn slice_122_grace_policy_error_labels_are_stable() {
4347 assert_eq!(
4348 RevokeGracePolicyError::GraceExceedsMaximum.as_str(),
4349 "grace_exceeds_maximum"
4350 );
4351 assert_eq!(
4352 RevokeGracePolicyError::CheckpointHooksRequireGrace.as_str(),
4353 "checkpoint_hooks_require_grace"
4354 );
4355 assert_eq!(
4356 RevokeGracePolicyError::DeadlineOverflow.as_str(),
4357 "deadline_overflow"
4358 );
4359
4360 for err in [
4361 RevokeGracePolicyError::GraceExceedsMaximum,
4362 RevokeGracePolicyError::CheckpointHooksRequireGrace,
4363 RevokeGracePolicyError::DeadlineOverflow,
4364 ] {
4365 assert_eq!(format!("{err}"), err.as_str());
4366 assert_ne!(err.as_str(), err.human_summary());
4367 }
4368 }
4369
4370 #[test]
4378 fn preemptibility_as_str_is_stable() {
4379 assert_eq!(Preemptibility::Preemptible.as_str(), "preemptible");
4380 assert_eq!(Preemptibility::Protected.as_str(), "protected");
4381 }
4382
4383 #[test]
4387 fn preemptibility_human_summary_is_stable() {
4388 assert_eq!(
4389 Preemptibility::Preemptible.human_summary(),
4390 "accepts preemption per priority rules (default)"
4391 );
4392 assert_eq!(
4393 Preemptibility::Protected.human_summary(),
4394 "non-preemptible by ordinary scheduling; operator-declared"
4395 );
4396 }
4397
4398 #[test]
4400 fn preemptibility_count_and_distinct() {
4401 let all = [Preemptibility::Preemptible, Preemptibility::Protected];
4402 assert_eq!(all.len(), 2, "Preemptibility must have exactly 2 variants");
4403
4404 let labels: Vec<&str> = all.iter().map(|p| p.as_str()).collect();
4405 let mut sorted = labels.clone();
4406 sorted.sort_unstable();
4407 sorted.dedup();
4408 assert_eq!(sorted.len(), labels.len(), "as_str labels must be distinct");
4409
4410 let summaries: Vec<&str> = all.iter().map(|p| p.human_summary()).collect();
4411 let mut sorted_s = summaries.clone();
4412 sorted_s.sort_unstable();
4413 sorted_s.dedup();
4414 assert_eq!(
4415 sorted_s.len(),
4416 summaries.len(),
4417 "human_summary strings must be distinct"
4418 );
4419 }
4420
4421 #[test]
4426 fn preemptibility_allows_preemption() {
4427 assert!(Preemptibility::Preemptible.allows_preemption());
4428 assert!(!Preemptibility::Protected.allows_preemption());
4429 }
4430
4431 #[test]
4434 fn preemptibility_display_matches_as_str() {
4435 assert_eq!(format!("{}", Preemptibility::Preemptible), "preemptible");
4436 assert_eq!(format!("{}", Preemptibility::Protected), "protected");
4437 }
4438
4439 #[test]
4443 fn preemptibility_human_summary_differs_from_as_str() {
4444 for p in [Preemptibility::Preemptible, Preemptibility::Protected] {
4445 assert_ne!(
4446 p.as_str(),
4447 p.human_summary(),
4448 "{p:?}: as_str and human_summary must be different surfaces"
4449 );
4450 }
4451 }
4452
4453 #[test]
4463 fn non_preemptible_reason_as_str_is_stable() {
4464 assert_eq!(
4465 NonPreemptibleReason::CheckpointInProgress.as_str(),
4466 "checkpoint_in_progress"
4467 );
4468 assert_eq!(
4469 NonPreemptibleReason::DataPlaneActive.as_str(),
4470 "data_plane_active"
4471 );
4472 assert_eq!(NonPreemptibleReason::OperatorPin.as_str(), "operator_pin");
4473 assert_eq!(
4474 NonPreemptibleReason::AttestationLocked.as_str(),
4475 "attestation_locked"
4476 );
4477 }
4478
4479 #[test]
4482 fn non_preemptible_reason_human_summary_is_stable() {
4483 assert_eq!(
4484 NonPreemptibleReason::CheckpointInProgress.human_summary(),
4485 "lease is mid-checkpoint; preempting would lose the checkpoint"
4486 );
4487 assert_eq!(
4488 NonPreemptibleReason::DataPlaneActive.human_summary(),
4489 "data-plane binding active; teardown requires coordination"
4490 );
4491 assert_eq!(
4492 NonPreemptibleReason::OperatorPin.human_summary(),
4493 "operator manually pinned this lease as non-preemptible"
4494 );
4495 assert_eq!(
4496 NonPreemptibleReason::AttestationLocked.human_summary(),
4497 "attestation chain requires this specific lease holder"
4498 );
4499 }
4500
4501 #[test]
4503 fn non_preemptible_reason_count_and_distinct() {
4504 let all = [
4505 NonPreemptibleReason::CheckpointInProgress,
4506 NonPreemptibleReason::DataPlaneActive,
4507 NonPreemptibleReason::OperatorPin,
4508 NonPreemptibleReason::AttestationLocked,
4509 ];
4510 assert_eq!(
4511 all.len(),
4512 4,
4513 "NonPreemptibleReason must have exactly 4 variants"
4514 );
4515
4516 let labels: Vec<&str> = all.iter().map(|r| r.as_str()).collect();
4517 let mut sorted = labels.clone();
4518 sorted.sort_unstable();
4519 sorted.dedup();
4520 assert_eq!(sorted.len(), labels.len(), "as_str labels must be distinct");
4521
4522 let summaries: Vec<&str> = all.iter().map(|r| r.human_summary()).collect();
4523 let mut sorted_s = summaries.clone();
4524 sorted_s.sort_unstable();
4525 sorted_s.dedup();
4526 assert_eq!(
4527 sorted_s.len(),
4528 summaries.len(),
4529 "human_summary strings must be distinct"
4530 );
4531 }
4532
4533 #[test]
4538 fn non_preemptible_reason_load_bearing_phrases() {
4539 assert!(
4540 NonPreemptibleReason::CheckpointInProgress
4541 .human_summary()
4542 .contains("checkpoint"),
4543 "CheckpointInProgress summary should mention 'checkpoint'"
4544 );
4545 assert!(
4546 NonPreemptibleReason::DataPlaneActive
4547 .human_summary()
4548 .contains("data-plane"),
4549 "DataPlaneActive summary should mention 'data-plane'"
4550 );
4551 assert!(
4552 NonPreemptibleReason::OperatorPin
4553 .human_summary()
4554 .contains("operator"),
4555 "OperatorPin summary should mention 'operator'"
4556 );
4557 assert!(
4558 NonPreemptibleReason::AttestationLocked
4559 .human_summary()
4560 .contains("attestation"),
4561 "AttestationLocked summary should mention 'attestation'"
4562 );
4563 }
4564
4565 #[test]
4568 fn non_preemptible_reason_display_matches_as_str() {
4569 assert_eq!(
4570 format!("{}", NonPreemptibleReason::CheckpointInProgress),
4571 "checkpoint_in_progress"
4572 );
4573 assert_eq!(
4574 format!("{}", NonPreemptibleReason::DataPlaneActive),
4575 "data_plane_active"
4576 );
4577 assert_eq!(
4578 format!("{}", NonPreemptibleReason::OperatorPin),
4579 "operator_pin"
4580 );
4581 assert_eq!(
4582 format!("{}", NonPreemptibleReason::AttestationLocked),
4583 "attestation_locked"
4584 );
4585 }
4586
4587 #[test]
4591 fn non_preemptible_reason_human_summary_differs_from_as_str() {
4592 for r in [
4593 NonPreemptibleReason::CheckpointInProgress,
4594 NonPreemptibleReason::DataPlaneActive,
4595 NonPreemptibleReason::OperatorPin,
4596 NonPreemptibleReason::AttestationLocked,
4597 ] {
4598 assert_ne!(
4599 r.as_str(),
4600 r.human_summary(),
4601 "{r:?}: as_str and human_summary must be different surfaces"
4602 );
4603 }
4604 }
4605
4606 #[test]
4614 fn hardware_generation_as_str_is_stable() {
4615 assert_eq!(HardwareGeneration::NvidiaAmpere.as_str(), "nvidia_ampere");
4616 assert_eq!(HardwareGeneration::NvidiaHopper.as_str(), "nvidia_hopper");
4617 assert_eq!(
4618 HardwareGeneration::NvidiaBlackwell.as_str(),
4619 "nvidia_blackwell"
4620 );
4621 assert_eq!(HardwareGeneration::AmdMi300.as_str(), "amd_mi300");
4622 assert_eq!(HardwareGeneration::AmdMi325.as_str(), "amd_mi325");
4623 assert_eq!(HardwareGeneration::IntelGaudi.as_str(), "intel_gaudi");
4624 assert_eq!(HardwareGeneration::Other.as_str(), "other");
4625 }
4626
4627 #[test]
4631 fn hardware_generation_human_summary_is_stable() {
4632 assert_eq!(
4633 HardwareGeneration::NvidiaAmpere.human_summary(),
4634 "NVIDIA Ampere (A100, A30, A40)"
4635 );
4636 assert_eq!(
4637 HardwareGeneration::NvidiaHopper.human_summary(),
4638 "NVIDIA Hopper (H100, H200)"
4639 );
4640 assert_eq!(
4641 HardwareGeneration::NvidiaBlackwell.human_summary(),
4642 "NVIDIA Blackwell (B200, GB200)"
4643 );
4644 assert_eq!(
4645 HardwareGeneration::AmdMi300.human_summary(),
4646 "AMD CDNA3 (MI300, MI300X, MI300A)"
4647 );
4648 assert_eq!(
4649 HardwareGeneration::AmdMi325.human_summary(),
4650 "AMD CDNA4 (MI325X and newer)"
4651 );
4652 assert_eq!(
4653 HardwareGeneration::IntelGaudi.human_summary(),
4654 "Intel Gaudi 2 / Gaudi 3"
4655 );
4656 assert_eq!(
4657 HardwareGeneration::Other.human_summary(),
4658 "other / generic accelerator family"
4659 );
4660 }
4661
4662 #[test]
4664 fn hardware_generation_count_and_distinct() {
4665 let all = [
4666 HardwareGeneration::NvidiaAmpere,
4667 HardwareGeneration::NvidiaHopper,
4668 HardwareGeneration::NvidiaBlackwell,
4669 HardwareGeneration::AmdMi300,
4670 HardwareGeneration::AmdMi325,
4671 HardwareGeneration::IntelGaudi,
4672 HardwareGeneration::Other,
4673 ];
4674 assert_eq!(
4675 all.len(),
4676 7,
4677 "HardwareGeneration must have exactly 7 variants"
4678 );
4679
4680 let labels: Vec<&str> = all.iter().map(|g| g.as_str()).collect();
4681 let mut sorted = labels.clone();
4682 sorted.sort_unstable();
4683 sorted.dedup();
4684 assert_eq!(sorted.len(), labels.len(), "as_str labels must be distinct");
4685
4686 let summaries: Vec<&str> = all.iter().map(|g| g.human_summary()).collect();
4687 let mut sorted_s = summaries.clone();
4688 sorted_s.sort_unstable();
4689 sorted_s.dedup();
4690 assert_eq!(
4691 sorted_s.len(),
4692 summaries.len(),
4693 "human_summary strings must be distinct"
4694 );
4695 }
4696
4697 #[test]
4701 fn hardware_generation_load_bearing_phrases() {
4702 assert!(
4703 HardwareGeneration::NvidiaAmpere
4704 .human_summary()
4705 .contains("Ampere"),
4706 "NvidiaAmpere summary should mention 'Ampere'"
4707 );
4708 assert!(
4709 HardwareGeneration::NvidiaHopper
4710 .human_summary()
4711 .contains("Hopper"),
4712 "NvidiaHopper summary should mention 'Hopper'"
4713 );
4714 assert!(
4715 HardwareGeneration::NvidiaBlackwell
4716 .human_summary()
4717 .contains("Blackwell"),
4718 "NvidiaBlackwell summary should mention 'Blackwell'"
4719 );
4720 assert!(
4721 HardwareGeneration::AmdMi300
4722 .human_summary()
4723 .contains("MI300"),
4724 "AmdMi300 summary should mention 'MI300'"
4725 );
4726 assert!(
4727 HardwareGeneration::AmdMi325
4728 .human_summary()
4729 .contains("MI325"),
4730 "AmdMi325 summary should mention 'MI325'"
4731 );
4732 assert!(
4733 HardwareGeneration::IntelGaudi
4734 .human_summary()
4735 .contains("Gaudi"),
4736 "IntelGaudi summary should mention 'Gaudi'"
4737 );
4738 }
4739
4740 #[test]
4743 fn hardware_generation_display_matches_as_str() {
4744 for g in [
4745 HardwareGeneration::NvidiaAmpere,
4746 HardwareGeneration::NvidiaHopper,
4747 HardwareGeneration::NvidiaBlackwell,
4748 HardwareGeneration::AmdMi300,
4749 HardwareGeneration::AmdMi325,
4750 HardwareGeneration::IntelGaudi,
4751 HardwareGeneration::Other,
4752 ] {
4753 assert_eq!(format!("{}", g), g.as_str());
4754 }
4755 }
4756
4757 #[test]
4758 fn hardware_generation_from_str_accepts_canonical_labels() {
4759 use core::str::FromStr;
4760
4761 for generation in [
4762 HardwareGeneration::NvidiaAmpere,
4763 HardwareGeneration::NvidiaHopper,
4764 HardwareGeneration::NvidiaBlackwell,
4765 HardwareGeneration::AmdMi300,
4766 HardwareGeneration::AmdMi325,
4767 HardwareGeneration::IntelGaudi,
4768 HardwareGeneration::Other,
4769 ] {
4770 assert_eq!(
4771 HardwareGeneration::from_str(generation.as_str()),
4772 Ok(generation)
4773 );
4774 }
4775 }
4776
4777 #[test]
4778 fn hardware_generation_from_str_unknown_label_fails_closed() {
4779 use core::str::FromStr;
4780
4781 let err = HardwareGeneration::from_str("nvidia-hopper").expect_err("loose label must fail");
4782 assert_eq!(err, ParseHardwareGenerationError("nvidia-hopper".into()));
4783 }
4784
4785 #[test]
4790 fn quota_schema_default_is_unlimited() {
4791 let schema = QuotaSchema::default();
4792 assert!(schema.is_unlimited());
4793 assert_eq!(
4794 schema.limit_for_resource_kind(crate::ResourceKind::Mem),
4795 None
4796 );
4797 assert_eq!(
4798 schema.limit_for_resource_kind(crate::ResourceKind::Gpu),
4799 None
4800 );
4801 }
4802
4803 #[test]
4804 fn quota_schema_maps_resource_kind_limits() {
4805 let schema = QuotaSchema {
4806 mem_bytes: Some(1024),
4807 cpu_cores: Some(8),
4808 gpu_count: Some(2),
4809 gpu_vram_bytes: Some(80 * 1024 * 1024 * 1024),
4810 gpu_count_by_generation: Vec::new(),
4811 block_bytes: Some(4096),
4812 net_bps: Some(10_000_000_000),
4813 tasklet_concurrency: Some(64),
4814 lease_create_per_minute: Some(120),
4815 burst_credits: None,
4816 fair_share: None,
4817 };
4818
4819 assert!(!schema.is_unlimited());
4820 assert_eq!(
4821 schema.limit_for_resource_kind(crate::ResourceKind::Mem),
4822 Some(1024)
4823 );
4824 assert_eq!(
4825 schema.limit_for_resource_kind(crate::ResourceKind::Cpu),
4826 Some(8)
4827 );
4828 assert_eq!(
4829 schema.limit_for_resource_kind(crate::ResourceKind::Gpu),
4830 Some(2)
4831 );
4832 assert_eq!(
4833 schema.limit_for_resource_kind(crate::ResourceKind::GpuMem),
4834 Some(80 * 1024 * 1024 * 1024)
4835 );
4836 assert_eq!(
4837 schema.limit_for_resource_kind(crate::ResourceKind::Block),
4838 Some(4096)
4839 );
4840 assert_eq!(
4841 schema.limit_for_resource_kind(crate::ResourceKind::Net),
4842 Some(10_000_000_000)
4843 );
4844 assert_eq!(
4845 schema.limit_for_resource_kind(crate::ResourceKind::Tasklet),
4846 Some(64)
4847 );
4848 }
4849
4850 #[test]
4851 fn quota_schema_gpu_generation_limits_are_typed() {
4852 let schema = QuotaSchema {
4853 gpu_count: None,
4854 gpu_count_by_generation: vec![
4855 GpuGenerationQuota {
4856 generation: HardwareGeneration::NvidiaAmpere,
4857 count: 2,
4858 },
4859 GpuGenerationQuota {
4860 generation: HardwareGeneration::NvidiaHopper,
4861 count: 4,
4862 },
4863 ],
4864 ..QuotaSchema::default()
4865 };
4866
4867 assert_eq!(schema.total_gpu_count(), Some(6));
4868 assert_eq!(
4869 schema.limit_for_resource_kind(crate::ResourceKind::Gpu),
4870 Some(6)
4871 );
4872 assert_eq!(
4873 schema.gpu_limit_for_generation(HardwareGeneration::NvidiaAmpere),
4874 Some(2)
4875 );
4876 assert_eq!(
4877 schema.gpu_limit_for_generation(HardwareGeneration::NvidiaBlackwell),
4878 None
4879 );
4880 }
4881
4882 #[test]
4883 fn quota_schema_total_gpu_count_prefers_explicit_total() {
4884 let schema = QuotaSchema {
4885 gpu_count: Some(10),
4886 gpu_count_by_generation: vec![GpuGenerationQuota {
4887 generation: HardwareGeneration::NvidiaHopper,
4888 count: 4,
4889 }],
4890 ..QuotaSchema::default()
4891 };
4892
4893 assert_eq!(schema.total_gpu_count(), Some(10));
4894 }
4895
4896 #[test]
4897 fn quota_usage_maps_resource_kind_usage() {
4898 let usage = QuotaUsage {
4899 mem_bytes: 1024,
4900 cpu_cores: 8,
4901 gpu_count: 2,
4902 gpu_vram_bytes: 80,
4903 gpu_count_by_generation: vec![GpuGenerationQuota {
4904 generation: HardwareGeneration::NvidiaHopper,
4905 count: 2,
4906 }],
4907 block_bytes: 4096,
4908 net_bps: 10_000,
4909 tasklet_concurrency: 64,
4910 lease_creates_in_window: 12,
4911 };
4912
4913 assert_eq!(
4914 usage.usage_for_resource_kind(crate::ResourceKind::Mem),
4915 1024
4916 );
4917 assert_eq!(usage.usage_for_resource_kind(crate::ResourceKind::Gpu), 2);
4918 assert_eq!(
4919 usage.usage_for_resource_kind(crate::ResourceKind::GpuMem),
4920 80
4921 );
4922 assert_eq!(
4923 usage.gpu_usage_for_generation(HardwareGeneration::NvidiaHopper),
4924 2
4925 );
4926 assert_eq!(
4927 usage.gpu_usage_for_generation(HardwareGeneration::NvidiaAmpere),
4928 0
4929 );
4930 }
4931
4932 #[test]
4933 fn burst_credit_policy_validates_progress_and_window() {
4934 assert!(BurstCreditPolicy {
4935 capacity: 100,
4936 refill_per_minute: 10,
4937 window_secs: 60,
4938 }
4939 .is_valid());
4940
4941 assert!(!BurstCreditPolicy {
4942 capacity: 0,
4943 refill_per_minute: 10,
4944 window_secs: 60,
4945 }
4946 .is_valid());
4947 assert!(!BurstCreditPolicy {
4948 capacity: 100,
4949 refill_per_minute: 0,
4950 window_secs: 60,
4951 }
4952 .is_valid());
4953 assert!(!BurstCreditPolicy {
4954 capacity: 100,
4955 refill_per_minute: 10,
4956 window_secs: 0,
4957 }
4958 .is_valid());
4959 }
4960
4961 #[test]
4962 fn quota_violation_as_str_is_stable() {
4963 assert_eq!(
4964 QuotaViolation::MemBytesExceeded {
4965 limit: 1,
4966 used: 2,
4967 requested: 3,
4968 }
4969 .as_str(),
4970 "mem_bytes_exceeded"
4971 );
4972 assert_eq!(
4973 QuotaViolation::CpuCoresExceeded {
4974 limit: 1,
4975 used: 2,
4976 requested: 3,
4977 }
4978 .as_str(),
4979 "cpu_cores_exceeded"
4980 );
4981 assert_eq!(
4982 QuotaViolation::GpuCountExceeded {
4983 generation: Some(HardwareGeneration::NvidiaHopper),
4984 limit: 1,
4985 used: 2,
4986 requested: 3,
4987 }
4988 .as_str(),
4989 "gpu_count_exceeded"
4990 );
4991 assert_eq!(
4992 QuotaViolation::GpuVramBytesExceeded {
4993 limit: 1,
4994 used: 2,
4995 requested: 3,
4996 }
4997 .as_str(),
4998 "gpu_vram_bytes_exceeded"
4999 );
5000 assert_eq!(
5001 QuotaViolation::BlockBytesExceeded {
5002 limit: 1,
5003 used: 2,
5004 requested: 3,
5005 }
5006 .as_str(),
5007 "block_bytes_exceeded"
5008 );
5009 assert_eq!(
5010 QuotaViolation::NetBpsExceeded {
5011 limit: 1,
5012 used: 2,
5013 requested: 3,
5014 }
5015 .as_str(),
5016 "net_bps_exceeded"
5017 );
5018 assert_eq!(
5019 QuotaViolation::TaskletConcurrencyExceeded {
5020 limit: 1,
5021 used: 2,
5022 requested: 3,
5023 }
5024 .as_str(),
5025 "tasklet_concurrency_exceeded"
5026 );
5027 assert_eq!(
5028 QuotaViolation::LeaseCreateRateExceeded {
5029 limit_per_minute: 1,
5030 used_in_window: 2,
5031 requested: 3,
5032 }
5033 .as_str(),
5034 "lease_create_rate_exceeded"
5035 );
5036 assert_eq!(
5037 QuotaViolation::ActiveLeaseCountExceeded {
5038 limit: 1,
5039 active: 2,
5040 requested: 1,
5041 }
5042 .as_str(),
5043 "active_lease_count_exceeded"
5044 );
5045 assert_eq!(
5046 QuotaViolation::PerNodeCapacityExceeded {
5047 limit: 1,
5048 used: 2,
5049 requested: 3,
5050 }
5051 .as_str(),
5052 "per_node_capacity_exceeded"
5053 );
5054 assert_eq!(
5055 QuotaViolation::BurstCreditExhausted {
5056 capacity: 1,
5057 remaining: 0,
5058 requested: 3,
5059 }
5060 .as_str(),
5061 "burst_credit_exhausted"
5062 );
5063 assert_eq!(
5064 QuotaViolation::FairShareExceeded {
5065 window_secs: 60,
5066 max_share_secs: 10,
5067 used_share_secs: 10,
5068 requested_share_secs: 1,
5069 }
5070 .as_str(),
5071 "fair_share_exceeded"
5072 );
5073 assert_eq!(
5074 QuotaViolation::TenantQuotaMissing.as_str(),
5075 "tenant_quota_missing"
5076 );
5077 }
5078
5079 #[test]
5080 fn quota_violation_display_matches_as_str() {
5081 let violation = QuotaViolation::MemBytesExceeded {
5082 limit: 1,
5083 used: 2,
5084 requested: 3,
5085 };
5086 assert_eq!(format!("{violation}"), violation.as_str());
5087 }
5088
5089 #[test]
5090 fn quota_violation_rejection_reason_mapping_is_stable() {
5091 assert_eq!(
5092 QuotaViolation::TenantQuotaMissing.rejection_reason(),
5093 RejectionReason::TenantNotFound
5094 );
5095 assert_eq!(
5096 QuotaViolation::BurstCreditExhausted {
5097 capacity: 10,
5098 remaining: 0,
5099 requested: 1,
5100 }
5101 .rejection_reason(),
5102 RejectionReason::QuotaBurstExceeded
5103 );
5104 assert_eq!(
5105 QuotaViolation::CpuCoresExceeded {
5106 limit: 1,
5107 used: 1,
5108 requested: 1,
5109 }
5110 .rejection_reason(),
5111 RejectionReason::QuotaHardLimitExceeded
5112 );
5113 }
5114
5115 #[cfg(feature = "serde")]
5116 #[test]
5117 fn quota_schema_serde_round_trip_uses_snake_case_fields() {
5118 let schema = QuotaSchema {
5119 mem_bytes: Some(1024),
5120 gpu_count_by_generation: vec![GpuGenerationQuota {
5121 generation: HardwareGeneration::NvidiaHopper,
5122 count: 2,
5123 }],
5124 burst_credits: Some(BurstCreditPolicy {
5125 capacity: 100,
5126 refill_per_minute: 10,
5127 window_secs: 60,
5128 }),
5129 ..QuotaSchema::default()
5130 };
5131
5132 let json = serde_json::to_string(&schema).expect("serialize quota schema");
5133 assert!(json.contains("\"mem_bytes\":1024"));
5134 assert!(json.contains("\"generation\":\"nvidia_hopper\""));
5135 assert!(json.contains("\"gpu_count_by_generation\""));
5136
5137 let back: QuotaSchema = serde_json::from_str(&json).expect("deserialize quota schema");
5138 assert_eq!(back, schema);
5139 }
5140
5141 #[test]
5144 fn bundle_atomicity_as_str_is_stable() {
5145 assert_eq!(BundleAtomicity::AllOrNothing.as_str(), "all_or_nothing");
5146 assert_eq!(BundleAtomicity::BestEffort.as_str(), "best_effort");
5147 }
5148
5149 #[test]
5151 fn bundle_atomicity_human_summary_is_stable() {
5152 assert_eq!(
5153 BundleAtomicity::AllOrNothing.human_summary(),
5154 "all-or-nothing: every requirement granted or none"
5155 );
5156 assert_eq!(
5157 BundleAtomicity::BestEffort.human_summary(),
5158 "best-effort: grant as many requirements as fit"
5159 );
5160 }
5161
5162 #[test]
5164 fn bundle_atomicity_count_and_distinct() {
5165 let all = [BundleAtomicity::AllOrNothing, BundleAtomicity::BestEffort];
5166 assert_eq!(all.len(), 2, "BundleAtomicity must have exactly 2 variants");
5167
5168 let labels: Vec<&str> = all.iter().map(|b| b.as_str()).collect();
5169 let mut sorted = labels.clone();
5170 sorted.sort_unstable();
5171 sorted.dedup();
5172 assert_eq!(sorted.len(), labels.len(), "as_str labels must be distinct");
5173
5174 let summaries: Vec<&str> = all.iter().map(|b| b.human_summary()).collect();
5175 let mut sorted_s = summaries.clone();
5176 sorted_s.sort_unstable();
5177 sorted_s.dedup();
5178 assert_eq!(
5179 sorted_s.len(),
5180 summaries.len(),
5181 "human_summary strings must be distinct"
5182 );
5183 }
5184
5185 #[test]
5188 fn bundle_atomicity_default_is_all_or_nothing() {
5189 assert_eq!(BundleAtomicity::default(), BundleAtomicity::AllOrNothing);
5190 }
5191
5192 #[test]
5194 fn bundle_atomicity_display_matches_as_str() {
5195 assert_eq!(
5196 format!("{}", BundleAtomicity::AllOrNothing),
5197 "all_or_nothing"
5198 );
5199 assert_eq!(format!("{}", BundleAtomicity::BestEffort), "best_effort");
5200 }
5201
5202 #[test]
5206 fn bundle_admission_failure_as_str_is_stable() {
5207 assert_eq!(BundleAdmissionFailure::EmptyBundle.as_str(), "empty_bundle");
5208 assert_eq!(
5209 BundleAdmissionFailure::InsufficientBundleCapacity.as_str(),
5210 "insufficient_bundle_capacity"
5211 );
5212 assert_eq!(
5213 BundleAdmissionFailure::BundleConstraintViolation.as_str(),
5214 "bundle_constraint_violation"
5215 );
5216 assert_eq!(
5217 BundleAdmissionFailure::HardwareGenerationUnavailable.as_str(),
5218 "hardware_generation_unavailable"
5219 );
5220 assert_eq!(
5221 BundleAdmissionFailure::OtherBundleFailure.as_str(),
5222 "other_bundle_failure"
5223 );
5224 }
5225
5226 #[test]
5228 fn bundle_admission_failure_human_summary_is_stable() {
5229 assert_eq!(
5230 BundleAdmissionFailure::EmptyBundle.human_summary(),
5231 "bundle had zero requirements; nothing to admit"
5232 );
5233 assert_eq!(
5234 BundleAdmissionFailure::InsufficientBundleCapacity.human_summary(),
5235 "insufficient capacity for at least one requirement; all-or-nothing rejected"
5236 );
5237 assert_eq!(
5238 BundleAdmissionFailure::BundleConstraintViolation.human_summary(),
5239 "placement constraint excludes every candidate for at least one requirement"
5240 );
5241 assert_eq!(
5242 BundleAdmissionFailure::HardwareGenerationUnavailable.human_summary(),
5243 "requested hardware generation has no matching inventory in the cluster"
5244 );
5245 assert_eq!(
5246 BundleAdmissionFailure::OtherBundleFailure.human_summary(),
5247 "other bundle admission failure; reserved catch-all"
5248 );
5249 }
5250
5251 #[test]
5253 fn bundle_admission_failure_count_and_distinct() {
5254 let all = [
5255 BundleAdmissionFailure::EmptyBundle,
5256 BundleAdmissionFailure::InsufficientBundleCapacity,
5257 BundleAdmissionFailure::BundleConstraintViolation,
5258 BundleAdmissionFailure::HardwareGenerationUnavailable,
5259 BundleAdmissionFailure::OtherBundleFailure,
5260 ];
5261 assert_eq!(
5262 all.len(),
5263 5,
5264 "BundleAdmissionFailure must have exactly 5 variants"
5265 );
5266
5267 let labels: Vec<&str> = all.iter().map(|r| r.as_str()).collect();
5268 let mut sorted = labels.clone();
5269 sorted.sort_unstable();
5270 sorted.dedup();
5271 assert_eq!(sorted.len(), labels.len(), "as_str labels must be distinct");
5272
5273 let summaries: Vec<&str> = all.iter().map(|r| r.human_summary()).collect();
5274 let mut sorted_s = summaries.clone();
5275 sorted_s.sort_unstable();
5276 sorted_s.dedup();
5277 assert_eq!(
5278 sorted_s.len(),
5279 summaries.len(),
5280 "human_summary strings must be distinct"
5281 );
5282 }
5283
5284 #[test]
5287 fn bundle_admission_failure_load_bearing_phrases() {
5288 assert!(
5289 BundleAdmissionFailure::EmptyBundle
5290 .human_summary()
5291 .contains("empty")
5292 || BundleAdmissionFailure::EmptyBundle
5293 .human_summary()
5294 .contains("zero"),
5295 "EmptyBundle summary should mention 'empty' or 'zero'"
5296 );
5297 assert!(
5298 BundleAdmissionFailure::InsufficientBundleCapacity
5299 .human_summary()
5300 .contains("capacity"),
5301 "InsufficientBundleCapacity summary should mention 'capacity'"
5302 );
5303 assert!(
5304 BundleAdmissionFailure::BundleConstraintViolation
5305 .human_summary()
5306 .contains("placement")
5307 || BundleAdmissionFailure::BundleConstraintViolation
5308 .human_summary()
5309 .contains("constraint"),
5310 "BundleConstraintViolation summary should mention 'placement' or 'constraint'"
5311 );
5312 assert!(
5313 BundleAdmissionFailure::HardwareGenerationUnavailable
5314 .human_summary()
5315 .contains("hardware generation")
5316 || BundleAdmissionFailure::HardwareGenerationUnavailable
5317 .human_summary()
5318 .contains("inventory"),
5319 "HardwareGenerationUnavailable summary should mention 'hardware generation' or 'inventory'"
5320 );
5321 assert!(
5322 BundleAdmissionFailure::OtherBundleFailure
5323 .human_summary()
5324 .contains("catch-all")
5325 || BundleAdmissionFailure::OtherBundleFailure
5326 .human_summary()
5327 .contains("other"),
5328 "OtherBundleFailure summary should mention 'catch-all' or 'other'"
5329 );
5330 }
5331
5332 #[test]
5334 fn bundle_admission_failure_display_matches_as_str() {
5335 for r in [
5336 BundleAdmissionFailure::EmptyBundle,
5337 BundleAdmissionFailure::InsufficientBundleCapacity,
5338 BundleAdmissionFailure::BundleConstraintViolation,
5339 BundleAdmissionFailure::HardwareGenerationUnavailable,
5340 BundleAdmissionFailure::OtherBundleFailure,
5341 ] {
5342 assert_eq!(format!("{}", r), r.as_str());
5343 }
5344 }
5345
5346 #[test]
5347 fn bundle_admission_failure_rejection_reason_fold_is_stable() {
5348 let cases = [
5349 (
5350 BundleAdmissionFailure::EmptyBundle,
5351 RejectionReason::EmptyBundle,
5352 ),
5353 (
5354 BundleAdmissionFailure::InsufficientBundleCapacity,
5355 RejectionReason::InsufficientBundleCapacity,
5356 ),
5357 (
5358 BundleAdmissionFailure::BundleConstraintViolation,
5359 RejectionReason::BundleConstraintViolation,
5360 ),
5361 (
5362 BundleAdmissionFailure::HardwareGenerationUnavailable,
5363 RejectionReason::HardwareGenerationUnavailable,
5364 ),
5365 (
5366 BundleAdmissionFailure::OtherBundleFailure,
5367 RejectionReason::OtherBundleFailure,
5368 ),
5369 ];
5370
5371 for (bundle_failure, rejection_reason) in cases {
5372 assert_eq!(bundle_failure.rejection_reason(), rejection_reason);
5373 assert_eq!(
5374 bundle_failure.as_str(),
5375 rejection_reason.as_str(),
5376 "bundle fold must preserve the stable admissions label"
5377 );
5378 }
5379 }
5380
5381 #[test]
5389 fn bundle_decision_as_str_is_stable() {
5390 assert_eq!(BundleDecision::Approved.as_str(), "approved");
5391 assert_eq!(
5392 BundleDecision::DeniedAllOrNothing.as_str(),
5393 "denied_all_or_nothing"
5394 );
5395 assert_eq!(BundleDecision::DeniedEmpty.as_str(), "denied_empty");
5396 assert_eq!(
5397 BundleDecision::PartialBestEffort.as_str(),
5398 "partial_best_effort"
5399 );
5400 }
5401
5402 #[test]
5405 fn bundle_decision_has_exactly_four_variants() {
5406 let all = [
5407 BundleDecision::Approved,
5408 BundleDecision::DeniedAllOrNothing,
5409 BundleDecision::DeniedEmpty,
5410 BundleDecision::PartialBestEffort,
5411 ];
5412 assert_eq!(all.len(), 4, "BundleDecision must have exactly 4 variants");
5413 let labels: Vec<&str> = all.iter().map(|d| d.as_str()).collect();
5414 let mut sorted = labels.clone();
5415 sorted.sort_unstable();
5416 sorted.dedup();
5417 assert_eq!(sorted.len(), labels.len(), "as_str labels must be distinct");
5418 }
5419
5420 #[test]
5422 fn bundle_decision_display_matches_as_str() {
5423 for d in [
5424 BundleDecision::Approved,
5425 BundleDecision::DeniedAllOrNothing,
5426 BundleDecision::DeniedEmpty,
5427 BundleDecision::PartialBestEffort,
5428 ] {
5429 assert_eq!(format!("{}", d), d.as_str());
5430 }
5431 }
5432
5433 #[test]
5436 fn bundle_decision_human_summary_differs_from_as_str() {
5437 for d in [
5438 BundleDecision::Approved,
5439 BundleDecision::DeniedAllOrNothing,
5440 BundleDecision::DeniedEmpty,
5441 BundleDecision::PartialBestEffort,
5442 ] {
5443 assert_ne!(
5444 d.as_str(),
5445 d.human_summary(),
5446 "{d:?}: as_str and human_summary must differ"
5447 );
5448 }
5449 }
5450
5451 #[test]
5454 fn bundle_admission_failure_human_summary_differs_from_as_str() {
5455 for r in [
5456 BundleAdmissionFailure::EmptyBundle,
5457 BundleAdmissionFailure::InsufficientBundleCapacity,
5458 BundleAdmissionFailure::BundleConstraintViolation,
5459 BundleAdmissionFailure::HardwareGenerationUnavailable,
5460 BundleAdmissionFailure::OtherBundleFailure,
5461 ] {
5462 assert_ne!(
5463 r.as_str(),
5464 r.human_summary(),
5465 "{r:?}: as_str and human_summary must be different surfaces"
5466 );
5467 }
5468 }
5469
5470 #[test]
5474 fn resource_bundle_spec_default_is_empty_all_or_nothing() {
5475 let spec = ResourceBundleSpec::default();
5476 assert!(
5477 spec.requirements.is_empty(),
5478 "Default ResourceBundleSpec must have no requirements"
5479 );
5480 assert_eq!(
5481 spec.atomicity,
5482 BundleAtomicity::AllOrNothing,
5483 "Default ResourceBundleSpec atomicity must be AllOrNothing"
5484 );
5485 }
5486
5487 #[test]
5491 fn resource_requirement_can_have_optional_hardware_generation() {
5492 let any = ResourceRequirement {
5494 kind: crate::ResourceKind::Gpu,
5495 capacity: 80 * 1024 * 1024 * 1024,
5496 hardware_generation: None,
5497 };
5498 assert!(any.hardware_generation.is_none());
5499
5500 let h100 = ResourceRequirement {
5502 kind: crate::ResourceKind::Gpu,
5503 capacity: 80 * 1024 * 1024 * 1024,
5504 hardware_generation: Some(HardwareGeneration::NvidiaHopper),
5505 };
5506 assert_eq!(
5507 h100.hardware_generation,
5508 Some(HardwareGeneration::NvidiaHopper)
5509 );
5510 }
5511
5512 #[cfg(feature = "serde")]
5518 #[test]
5519 fn resource_bundle_spec_round_trips_through_serde() {
5520 let spec = ResourceBundleSpec {
5521 requirements: vec![
5522 ResourceRequirement {
5523 kind: crate::ResourceKind::Cpu,
5524 capacity: 8,
5525 hardware_generation: None,
5526 },
5527 ResourceRequirement {
5528 kind: crate::ResourceKind::Gpu,
5529 capacity: 80 * 1024 * 1024 * 1024,
5530 hardware_generation: Some(HardwareGeneration::NvidiaHopper),
5531 },
5532 ],
5533 atomicity: BundleAtomicity::AllOrNothing,
5534 };
5535 let json = serde_json::to_string(&spec).expect("serialize");
5536 assert!(json.contains("\"atomicity\":\"all_or_nothing\""), "{json}");
5538 assert!(
5539 json.contains("\"hardware_generation\":\"nvidia_hopper\""),
5540 "{json}"
5541 );
5542 assert!(json.contains("\"hardware_generation\":null"), "{json}");
5543 let back: ResourceBundleSpec = serde_json::from_str(&json).expect("deserialize");
5544 assert_eq!(back, spec);
5545 }
5546
5547 #[cfg(feature = "serde")]
5575 #[test]
5576 fn snake_case_serde_matches_as_str_round_trip() {
5577 macro_rules! pin {
5578 ($($value:expr),* $(,)?) => {{
5579 $({
5580 let v = $value;
5581 let json = serde_json::to_string(&v).expect("serialize");
5582 let expected = format!("\"{}\"", v.as_str());
5583 assert_eq!(
5584 json, expected,
5585 "{:?}: serde wire shape must match as_str() (slice 90 \
5586 single-source-of-truth)",
5587 v
5588 );
5589 })*
5590 }};
5591 }
5592
5593 pin!(
5594 PreemptionReason::PriorityPreemption,
5596 PreemptionReason::QuotaRebalance,
5597 PreemptionReason::BurstCreditExhausted,
5598 PreemptionReason::BudgetExhausted,
5599 PreemptionReason::CostCapEviction,
5600 PreemptionReason::OperatorDrain,
5601 PreemptionReason::OperatorMigProfileChange,
5602 PreemptionReason::MaintenanceWindow,
5603 PreemptionReason::PolicyViolationRecovery,
5604 Priority::Scavenger,
5606 Priority::Standard,
5607 Priority::Guaranteed,
5608 RejectionReason::InsufficientCapacity,
5610 RejectionReason::NoEligibleNodes,
5611 RejectionReason::QuotaHardLimitExceeded,
5612 RejectionReason::QuotaBurstExceeded,
5613 RejectionReason::QuotaLeaseCountExceeded,
5614 RejectionReason::QuotaPerNodeLimitExceeded,
5615 RejectionReason::TenantNotFound,
5616 RejectionReason::NodeFenced,
5617 RejectionReason::BudgetExhausted,
5618 RejectionReason::ReservationExhausted,
5619 RejectionReason::PlacementPolicyExcluded,
5620 RejectionReason::NodeAddressUnknown,
5621 RejectionReason::NodeDrained,
5622 RejectionReason::EmptyBundle,
5623 RejectionReason::InsufficientBundleCapacity,
5624 RejectionReason::BundleConstraintViolation,
5625 RejectionReason::HardwareGenerationUnavailable,
5626 RejectionReason::OtherBundleFailure,
5627 EgressEnforcement::FabricEnforced,
5629 EgressEnforcement::HostRuntimeIntegration,
5630 EgressEnforcement::OperatorControlled,
5631 EgressEnforcement::Unsupported,
5632 EgressTarget::WasmTasklet,
5634 EgressTarget::NativeProgram,
5635 EgressTarget::ContainerAdjacent,
5636 EgressTarget::KubernetesDra,
5637 EgressTarget::BareMetal,
5638 EconomicsSource::OperatorStaticConfig,
5640 EconomicsSource::ProviderPriceSheet,
5641 EconomicsSource::ProviderUsageExport,
5642 EconomicsSource::MeasuredPowerTelemetry,
5643 EconomicsSource::ThirdPartyCarbonFeed,
5644 EconomicsSource::DerivedEstimate,
5645 ObservationConfidence::Low,
5647 ObservationConfidence::Medium,
5648 ObservationConfidence::High,
5649 RevokeState::Active,
5651 RevokeState::RevokeWarning,
5652 RevokeState::GraceRunning,
5653 RevokeState::CheckpointReported,
5654 RevokeState::ForcedTeardown,
5655 RevokeState::Torndown,
5656 RevokeState::Expired,
5657 RevokeState::Fenced,
5658 RevokeState::FailedClosed,
5659 Preemptibility::Preemptible,
5661 Preemptibility::Protected,
5662 NonPreemptibleReason::CheckpointInProgress,
5664 NonPreemptibleReason::DataPlaneActive,
5665 NonPreemptibleReason::OperatorPin,
5666 NonPreemptibleReason::AttestationLocked,
5667 HardwareGeneration::NvidiaAmpere,
5669 HardwareGeneration::NvidiaHopper,
5670 HardwareGeneration::NvidiaBlackwell,
5671 HardwareGeneration::AmdMi300,
5672 HardwareGeneration::AmdMi325,
5673 HardwareGeneration::IntelGaudi,
5674 HardwareGeneration::Other,
5675 BundleAtomicity::AllOrNothing,
5676 BundleAtomicity::BestEffort,
5677 BundleAdmissionFailure::EmptyBundle,
5678 BundleAdmissionFailure::InsufficientBundleCapacity,
5679 BundleAdmissionFailure::BundleConstraintViolation,
5680 BundleAdmissionFailure::HardwareGenerationUnavailable,
5681 BundleAdmissionFailure::OtherBundleFailure,
5682 BundleDecision::Approved,
5684 BundleDecision::DeniedAllOrNothing,
5685 BundleDecision::DeniedEmpty,
5686 BundleDecision::PartialBestEffort,
5687 AuditEventKind::CapabilityIssued,
5689 AuditEventKind::CapabilityRevoked,
5690 AuditEventKind::LeaseAllocated,
5691 AuditEventKind::LeaseRenewed,
5692 AuditEventKind::LeaseReleased,
5693 AuditEventKind::LeaseExpired,
5694 AuditEventKind::LeaseTorndown,
5695 AuditEventKind::LeaseFenced,
5696 AuditEventKind::AdmissionDecided,
5697 AuditEventKind::Preempted,
5698 AuditEventKind::DrainInitiated,
5699 AuditEventKind::ChainAnchored,
5700 AuditEventKind::SoftModeEnabled,
5701 AuditEventKind::TenantCreated,
5702 AuditEventKind::TenantDeleted,
5703 AuditEventKind::TenantQuotaUpdated,
5704 AuditEventKind::ProviderConformanceRecorded,
5705 AuditEventKind::ProviderBootstrapTokenIssued,
5706 AuditEventKind::ProviderBootstrapExchanged,
5707 AuditEventKind::ProviderCellIdentityIssued,
5708 AuditEventKind::ProviderCellIdentityRotated,
5709 AuditEventKind::ProviderCellIdentityRevoked,
5710 AuditEventKind::BearerTokenIssued,
5711 AuditEventKind::BearerTokenRevoked,
5712 AuditEventKind::SchedulerPromoted,
5713 AuditEventKind::BillingRateCardInstalled,
5714 );
5715 }
5716
5717 #[cfg(feature = "serde")]
5722 #[test]
5723 fn snake_case_serde_deserialize_round_trip() {
5724 macro_rules! pin {
5725 ($ty:ty: $($value:expr),* $(,)?) => {{
5726 $({
5727 let v = $value;
5728 let s = format!("\"{}\"", v.as_str());
5729 let back: $ty = serde_json::from_str(&s)
5730 .expect(&format!("must deserialize from snake_case for {:?}", v));
5731 assert_eq!(back, v, "round-trip mismatch for {:?}", v);
5732 })*
5733 }};
5734 }
5735
5736 pin!(PreemptionReason:
5737 PreemptionReason::PriorityPreemption,
5738 PreemptionReason::QuotaRebalance,
5739 PreemptionReason::BurstCreditExhausted,
5740 PreemptionReason::BudgetExhausted,
5741 PreemptionReason::CostCapEviction,
5742 PreemptionReason::OperatorDrain,
5743 PreemptionReason::OperatorMigProfileChange,
5744 PreemptionReason::MaintenanceWindow,
5745 PreemptionReason::PolicyViolationRecovery,
5746 );
5747 pin!(Priority:
5748 Priority::Scavenger,
5749 Priority::Standard,
5750 Priority::Guaranteed,
5751 );
5752 pin!(RejectionReason:
5753 RejectionReason::InsufficientCapacity,
5754 RejectionReason::NoEligibleNodes,
5755 RejectionReason::QuotaHardLimitExceeded,
5756 RejectionReason::QuotaBurstExceeded,
5757 RejectionReason::QuotaLeaseCountExceeded,
5758 RejectionReason::QuotaPerNodeLimitExceeded,
5759 RejectionReason::TenantNotFound,
5760 RejectionReason::NodeFenced,
5761 RejectionReason::BudgetExhausted,
5762 RejectionReason::ReservationExhausted,
5763 RejectionReason::PlacementPolicyExcluded,
5764 RejectionReason::NodeAddressUnknown,
5765 RejectionReason::NodeDrained,
5766 RejectionReason::EmptyBundle,
5767 RejectionReason::InsufficientBundleCapacity,
5768 RejectionReason::BundleConstraintViolation,
5769 RejectionReason::HardwareGenerationUnavailable,
5770 RejectionReason::OtherBundleFailure,
5771 );
5772 pin!(EgressEnforcement:
5773 EgressEnforcement::FabricEnforced,
5774 EgressEnforcement::HostRuntimeIntegration,
5775 EgressEnforcement::OperatorControlled,
5776 EgressEnforcement::Unsupported,
5777 );
5778 pin!(EgressTarget:
5779 EgressTarget::WasmTasklet,
5780 EgressTarget::NativeProgram,
5781 EgressTarget::ContainerAdjacent,
5782 EgressTarget::KubernetesDra,
5783 EgressTarget::BareMetal,
5784 );
5785 pin!(EconomicsSource:
5786 EconomicsSource::OperatorStaticConfig,
5787 EconomicsSource::ProviderPriceSheet,
5788 EconomicsSource::ProviderUsageExport,
5789 EconomicsSource::MeasuredPowerTelemetry,
5790 EconomicsSource::ThirdPartyCarbonFeed,
5791 EconomicsSource::DerivedEstimate,
5792 );
5793 pin!(ObservationConfidence:
5794 ObservationConfidence::Low,
5795 ObservationConfidence::Medium,
5796 ObservationConfidence::High,
5797 );
5798 pin!(RevokeState:
5799 RevokeState::Active,
5800 RevokeState::RevokeWarning,
5801 RevokeState::GraceRunning,
5802 RevokeState::CheckpointReported,
5803 RevokeState::ForcedTeardown,
5804 RevokeState::Torndown,
5805 RevokeState::Expired,
5806 RevokeState::Fenced,
5807 RevokeState::FailedClosed,
5808 );
5809 pin!(Preemptibility:
5810 Preemptibility::Preemptible,
5811 Preemptibility::Protected,
5812 );
5813 pin!(NonPreemptibleReason:
5814 NonPreemptibleReason::CheckpointInProgress,
5815 NonPreemptibleReason::DataPlaneActive,
5816 NonPreemptibleReason::OperatorPin,
5817 NonPreemptibleReason::AttestationLocked,
5818 );
5819 pin!(HardwareGeneration:
5824 HardwareGeneration::NvidiaAmpere,
5825 HardwareGeneration::NvidiaHopper,
5826 HardwareGeneration::NvidiaBlackwell,
5827 HardwareGeneration::AmdMi300,
5828 HardwareGeneration::AmdMi325,
5829 HardwareGeneration::IntelGaudi,
5830 HardwareGeneration::Other,
5831 );
5832 pin!(BundleAtomicity:
5833 BundleAtomicity::AllOrNothing,
5834 BundleAtomicity::BestEffort,
5835 );
5836 pin!(BundleAdmissionFailure:
5837 BundleAdmissionFailure::EmptyBundle,
5838 BundleAdmissionFailure::InsufficientBundleCapacity,
5839 BundleAdmissionFailure::BundleConstraintViolation,
5840 BundleAdmissionFailure::HardwareGenerationUnavailable,
5841 BundleAdmissionFailure::OtherBundleFailure,
5842 );
5843 pin!(BundleDecision:
5847 BundleDecision::Approved,
5848 BundleDecision::DeniedAllOrNothing,
5849 BundleDecision::DeniedEmpty,
5850 BundleDecision::PartialBestEffort,
5851 );
5852 pin!(AuditEventKind:
5853 AuditEventKind::CapabilityIssued,
5854 AuditEventKind::CapabilityRevoked,
5855 AuditEventKind::LeaseAllocated,
5856 AuditEventKind::LeaseRenewed,
5857 AuditEventKind::LeaseReleased,
5858 AuditEventKind::LeaseExpired,
5859 AuditEventKind::LeaseTorndown,
5860 AuditEventKind::LeaseFenced,
5861 AuditEventKind::AdmissionDecided,
5862 AuditEventKind::Preempted,
5863 AuditEventKind::DrainInitiated,
5864 AuditEventKind::ChainAnchored,
5865 AuditEventKind::SoftModeEnabled,
5866 AuditEventKind::TenantCreated,
5867 AuditEventKind::TenantDeleted,
5868 AuditEventKind::TenantQuotaUpdated,
5869 AuditEventKind::ProviderConformanceRecorded,
5870 AuditEventKind::ProviderBootstrapTokenIssued,
5871 AuditEventKind::ProviderBootstrapExchanged,
5872 AuditEventKind::ProviderCellIdentityIssued,
5873 AuditEventKind::ProviderCellIdentityRotated,
5874 AuditEventKind::ProviderCellIdentityRevoked,
5875 AuditEventKind::BearerTokenIssued,
5876 AuditEventKind::BearerTokenRevoked,
5877 AuditEventKind::SchedulerPromoted,
5878 AuditEventKind::BillingRateCardInstalled,
5879 );
5880 }
5881
5882 #[cfg(feature = "serde")]
5895 #[test]
5896 fn pascal_case_legacy_alias_round_trip() {
5897 macro_rules! pin {
5898 ($ty:ty: $($value:expr),* $(,)?) => {{
5899 $({
5900 let v = $value;
5901 let pascal = format!("{:?}", v);
5902 let json = format!("\"{}\"", pascal);
5903 let back: $ty = serde_json::from_str(&json).unwrap_or_else(|e| {
5904 panic!("legacy PascalCase {} must parse for {:?}: {}", json, v, e)
5905 });
5906 assert_eq!(back, v, "alias mismatch for legacy {} -> {:?}", json, v);
5907 })*
5908 }};
5909 }
5910
5911 pin!(PreemptionReason:
5912 PreemptionReason::PriorityPreemption,
5913 PreemptionReason::QuotaRebalance,
5914 PreemptionReason::BurstCreditExhausted,
5915 PreemptionReason::BudgetExhausted,
5916 PreemptionReason::CostCapEviction,
5917 PreemptionReason::OperatorDrain,
5918 PreemptionReason::OperatorMigProfileChange,
5919 PreemptionReason::MaintenanceWindow,
5920 PreemptionReason::PolicyViolationRecovery,
5921 );
5922 pin!(Priority:
5923 Priority::Scavenger,
5924 Priority::Standard,
5925 Priority::Guaranteed,
5926 );
5927 pin!(EvidenceLabel:
5928 EvidenceLabel::DesignTarget,
5929 EvidenceLabel::UnitIntegrationEvidence,
5930 EvidenceLabel::LabEvidence,
5931 EvidenceLabel::StagedProviderEvidence,
5932 EvidenceLabel::DesignPartnerEvidence,
5933 EvidenceLabel::ProductionEvidence,
5934 );
5935 pin!(RejectionReason:
5936 RejectionReason::InsufficientCapacity,
5937 RejectionReason::NoEligibleNodes,
5938 RejectionReason::QuotaHardLimitExceeded,
5939 RejectionReason::QuotaBurstExceeded,
5940 RejectionReason::QuotaLeaseCountExceeded,
5941 RejectionReason::QuotaPerNodeLimitExceeded,
5942 RejectionReason::TenantNotFound,
5943 RejectionReason::NodeFenced,
5944 RejectionReason::BudgetExhausted,
5945 RejectionReason::ReservationExhausted,
5946 RejectionReason::PlacementPolicyExcluded,
5947 RejectionReason::NodeAddressUnknown,
5948 RejectionReason::NodeDrained,
5949 RejectionReason::EmptyBundle,
5950 RejectionReason::InsufficientBundleCapacity,
5951 RejectionReason::BundleConstraintViolation,
5952 RejectionReason::HardwareGenerationUnavailable,
5953 RejectionReason::OtherBundleFailure,
5954 );
5955 pin!(EgressEnforcement:
5956 EgressEnforcement::FabricEnforced,
5957 EgressEnforcement::HostRuntimeIntegration,
5958 EgressEnforcement::OperatorControlled,
5959 EgressEnforcement::Unsupported,
5960 );
5961 pin!(EgressTarget:
5962 EgressTarget::WasmTasklet,
5963 EgressTarget::NativeProgram,
5964 EgressTarget::ContainerAdjacent,
5965 EgressTarget::KubernetesDra,
5966 EgressTarget::BareMetal,
5967 );
5968 pin!(EconomicsSource:
5969 EconomicsSource::OperatorStaticConfig,
5970 EconomicsSource::ProviderPriceSheet,
5971 EconomicsSource::ProviderUsageExport,
5972 EconomicsSource::MeasuredPowerTelemetry,
5973 EconomicsSource::ThirdPartyCarbonFeed,
5974 EconomicsSource::DerivedEstimate,
5975 );
5976 pin!(ObservationConfidence:
5977 ObservationConfidence::Low,
5978 ObservationConfidence::Medium,
5979 ObservationConfidence::High,
5980 );
5981 pin!(RevokeState:
5982 RevokeState::Active,
5983 RevokeState::RevokeWarning,
5984 RevokeState::GraceRunning,
5985 RevokeState::CheckpointReported,
5986 RevokeState::ForcedTeardown,
5987 RevokeState::Torndown,
5988 RevokeState::Expired,
5989 RevokeState::Fenced,
5990 RevokeState::FailedClosed,
5991 );
5992 pin!(Preemptibility:
5993 Preemptibility::Preemptible,
5994 Preemptibility::Protected,
5995 );
5996 pin!(NonPreemptibleReason:
5997 NonPreemptibleReason::CheckpointInProgress,
5998 NonPreemptibleReason::DataPlaneActive,
5999 NonPreemptibleReason::OperatorPin,
6000 NonPreemptibleReason::AttestationLocked,
6001 );
6002 pin!(AuditEventKind:
6003 AuditEventKind::CapabilityIssued,
6004 AuditEventKind::CapabilityRevoked,
6005 AuditEventKind::LeaseAllocated,
6006 AuditEventKind::LeaseRenewed,
6007 AuditEventKind::LeaseReleased,
6008 AuditEventKind::LeaseExpired,
6009 AuditEventKind::LeaseTorndown,
6010 AuditEventKind::LeaseFenced,
6011 AuditEventKind::AdmissionDecided,
6012 AuditEventKind::Preempted,
6013 AuditEventKind::DrainInitiated,
6014 AuditEventKind::ChainAnchored,
6015 AuditEventKind::SoftModeEnabled,
6016 AuditEventKind::TenantCreated,
6017 AuditEventKind::TenantDeleted,
6018 AuditEventKind::TenantQuotaUpdated,
6019 AuditEventKind::ProviderConformanceRecorded,
6020 AuditEventKind::ProviderBootstrapTokenIssued,
6021 AuditEventKind::ProviderBootstrapExchanged,
6022 AuditEventKind::ProviderCellIdentityIssued,
6023 AuditEventKind::ProviderCellIdentityRotated,
6024 AuditEventKind::ProviderCellIdentityRevoked,
6025 AuditEventKind::BearerTokenIssued,
6026 AuditEventKind::BearerTokenRevoked,
6027 AuditEventKind::SchedulerPromoted,
6028 AuditEventKind::BillingRateCardInstalled,
6029 );
6030 }
6031
6032 #[cfg(feature = "serde")]
6041 #[test]
6042 fn evidence_label_serde_wire_shape() {
6043 for (variant, expected) in [
6045 (EvidenceLabel::DesignTarget, "design_target"),
6046 (
6047 EvidenceLabel::UnitIntegrationEvidence,
6048 "unit_integration_evidence",
6049 ),
6050 (EvidenceLabel::LabEvidence, "lab_evidence"),
6051 (
6052 EvidenceLabel::StagedProviderEvidence,
6053 "staged_provider_evidence",
6054 ),
6055 (
6056 EvidenceLabel::DesignPartnerEvidence,
6057 "design_partner_evidence",
6058 ),
6059 (EvidenceLabel::ProductionEvidence, "production_evidence"),
6060 ] {
6061 let json = serde_json::to_string(&variant).expect("serialize");
6062 assert_eq!(
6063 json,
6064 format!("\"{}\"", expected),
6065 "{:?} wire shape mismatch",
6066 variant
6067 );
6068 let back: EvidenceLabel = serde_json::from_str(&json).expect("deserialize canonical");
6069 assert_eq!(back, variant);
6070 }
6071 }
6072
6073 #[cfg(feature = "serde")]
6078 #[test]
6079 fn snake_case_serde_rejects_unknown_strings() {
6080 assert!(serde_json::from_str::<Priority>("\"bogus\"").is_err());
6081 assert!(serde_json::from_str::<AuditEventKind>("\"not_a_kind\"").is_err());
6082 assert!(serde_json::from_str::<RevokeState>("\"in_between\"").is_err());
6083 assert!(serde_json::from_str::<RejectionReason>("\"placeholder\"").is_err());
6084 assert!(serde_json::from_str::<Preemptibility>("\"\"").is_err());
6085 assert!(serde_json::from_str::<NonPreemptibleReason>("\"unknown\"").is_err());
6086 assert!(serde_json::from_str::<HardwareGeneration>("\"unknown_gpu\"").is_err());
6089 assert!(serde_json::from_str::<BundleAtomicity>("\"maybe\"").is_err());
6090 assert!(serde_json::from_str::<BundleAdmissionFailure>("\"no_reason\"").is_err());
6091 }
6092}