1#![allow(missing_docs)]
2use crate::lab::config::LabConfig;
39use serde::{Deserialize, Serialize};
40use std::collections::BTreeMap;
41use std::fmt;
42
43fn derive_component_seed(root: u64, component: &str) -> u64 {
46 fnv1a_mix(root, component.as_bytes())
47}
48
49fn derive_scenario_seed(root: u64, scenario: &str) -> u64 {
50 let tag = format!("scenario:{scenario}");
51 fnv1a_mix(root, tag.as_bytes())
52}
53
54fn fnv1a_mix(root: u64, tag: &[u8]) -> u64 {
55 const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
56 const FNV_PRIME: u64 = 0x0100_0000_01b3;
57
58 let mut hash = FNV_OFFSET;
59 for byte in root.to_le_bytes() {
60 hash ^= u64::from(byte);
61 hash = hash.wrapping_mul(FNV_PRIME);
62 }
63 for &byte in tag {
64 hash ^= u64::from(byte);
65 hash = hash.wrapping_mul(FNV_PRIME);
66 }
67 hash
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
76#[serde(rename_all = "snake_case")]
77pub enum SeedMode {
78 Inherit,
80 Override,
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
88#[serde(rename_all = "snake_case")]
89pub enum ReplayPolicy {
90 SingleSeed,
92 SeedSweep,
95 ReplayBundle,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
110pub struct SeedPlan {
111 pub canonical_seed: u64,
113
114 pub seed_lineage_id: String,
117
118 pub lab_seed_mode: SeedMode,
120
121 pub live_seed_mode: SeedMode,
123
124 pub replay_policy: ReplayPolicy,
126
127 #[serde(skip_serializing_if = "Option::is_none")]
129 pub lab_seed_override: Option<u64>,
130
131 #[serde(skip_serializing_if = "Option::is_none")]
133 pub live_seed_override: Option<u64>,
134
135 #[serde(skip_serializing_if = "Option::is_none")]
138 pub entropy_seed_override: Option<u64>,
139}
140
141impl SeedPlan {
142 #[must_use]
144 pub fn inherit(canonical_seed: u64, lineage_id: impl Into<String>) -> Self {
145 Self {
146 canonical_seed,
147 seed_lineage_id: lineage_id.into(),
148 lab_seed_mode: SeedMode::Inherit,
149 live_seed_mode: SeedMode::Inherit,
150 replay_policy: ReplayPolicy::SingleSeed,
151 lab_seed_override: None,
152 live_seed_override: None,
153 entropy_seed_override: None,
154 }
155 }
156
157 #[must_use]
159 pub fn effective_lab_seed(&self) -> u64 {
160 match self.lab_seed_mode {
161 SeedMode::Inherit => self.canonical_seed,
162 SeedMode::Override => self.lab_seed_override.unwrap_or(self.canonical_seed),
163 }
164 }
165
166 #[must_use]
168 pub fn effective_live_seed(&self) -> u64 {
169 match self.live_seed_mode {
170 SeedMode::Inherit => self.canonical_seed,
171 SeedMode::Override => self.live_seed_override.unwrap_or(self.canonical_seed),
172 }
173 }
174
175 #[must_use]
179 pub fn effective_entropy_seed(&self, effective_seed: u64) -> u64 {
180 self.entropy_seed_override
181 .unwrap_or_else(|| derive_component_seed(effective_seed, "entropy"))
182 }
183
184 #[must_use]
188 pub fn to_lab_config(&self) -> LabConfig {
189 let seed = self.effective_lab_seed();
190 let entropy = self.effective_entropy_seed(seed);
191 LabConfig::new(seed).entropy_seed(entropy)
192 }
193
194 #[must_use]
200 pub fn sweep_seeds(&self, count: usize) -> Vec<u64> {
201 (0..count)
202 .map(|i| {
203 let tag = format!("sweep:{i}");
204 derive_scenario_seed(self.canonical_seed, &tag)
205 })
206 .collect()
207 }
208
209 #[must_use]
211 pub fn with_lab_override(mut self, seed: u64) -> Self {
212 self.lab_seed_mode = SeedMode::Override;
213 self.lab_seed_override = Some(seed);
214 self
215 }
216
217 #[must_use]
219 pub fn with_live_override(mut self, seed: u64) -> Self {
220 self.live_seed_mode = SeedMode::Override;
221 self.live_seed_override = Some(seed);
222 self
223 }
224
225 #[must_use]
227 pub fn with_replay_policy(mut self, policy: ReplayPolicy) -> Self {
228 self.replay_policy = policy;
229 self
230 }
231
232 #[must_use]
234 pub fn with_entropy_seed(mut self, seed: u64) -> Self {
235 self.entropy_seed_override = Some(seed);
236 self
237 }
238}
239
240impl fmt::Display for SeedPlan {
241 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242 write!(
243 f,
244 "SeedPlan(canonical=0x{:X}, lineage={}, lab={:?}, live={:?}, policy={:?})",
245 self.canonical_seed,
246 self.seed_lineage_id,
247 self.lab_seed_mode,
248 self.live_seed_mode,
249 self.replay_policy,
250 )
251 }
252}
253
254#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
264pub struct ScenarioFamilyId {
265 pub id: String,
267 pub surface_id: String,
269 pub surface_contract_version: String,
271}
272
273impl ScenarioFamilyId {
274 #[must_use]
276 pub fn new(
277 id: impl Into<String>,
278 surface_id: impl Into<String>,
279 contract_version: impl Into<String>,
280 ) -> Self {
281 Self {
282 id: id.into(),
283 surface_id: surface_id.into(),
284 surface_contract_version: contract_version.into(),
285 }
286 }
287}
288
289impl fmt::Display for ScenarioFamilyId {
290 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291 write!(
292 f,
293 "{}@{}({})",
294 self.id, self.surface_id, self.surface_contract_version
295 )
296 }
297}
298
299#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
305pub struct ExecutionInstanceId {
306 pub family_id: String,
308 pub effective_seed: u64,
310 pub runtime_kind: RuntimeKind,
312 pub run_index: u32,
314}
315
316impl ExecutionInstanceId {
317 #[must_use]
319 pub fn lab(family_id: impl Into<String>, seed: u64) -> Self {
320 Self {
321 family_id: family_id.into(),
322 effective_seed: seed,
323 runtime_kind: RuntimeKind::Lab,
324 run_index: 0,
325 }
326 }
327
328 #[must_use]
330 pub fn live(family_id: impl Into<String>, seed: u64) -> Self {
331 Self {
332 family_id: family_id.into(),
333 effective_seed: seed,
334 runtime_kind: RuntimeKind::Live,
335 run_index: 0,
336 }
337 }
338
339 #[must_use]
341 pub fn with_run_index(mut self, index: u32) -> Self {
342 self.run_index = index;
343 self
344 }
345
346 #[must_use]
348 pub fn key(&self) -> String {
349 format!(
350 "{}:{}:0x{:X}:{}",
351 self.family_id, self.runtime_kind, self.effective_seed, self.run_index
352 )
353 }
354}
355
356impl fmt::Display for ExecutionInstanceId {
357 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
358 write!(
359 f,
360 "{}[{}@0x{:X}#{}]",
361 self.family_id, self.runtime_kind, self.effective_seed, self.run_index
362 )
363 }
364}
365
366#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
368#[serde(rename_all = "snake_case")]
369pub enum RuntimeKind {
370 Lab,
372 Live,
374}
375
376impl fmt::Display for RuntimeKind {
377 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
378 match self {
379 Self::Lab => write!(f, "lab"),
380 Self::Live => write!(f, "live"),
381 }
382 }
383}
384
385#[derive(Debug, Clone, Serialize, Deserialize)]
398pub struct ReplayMetadata {
399 pub family: ScenarioFamilyId,
401
402 pub instance: ExecutionInstanceId,
404
405 pub seed_plan: SeedPlan,
407
408 pub effective_seed: u64,
410
411 pub effective_entropy_seed: u64,
413
414 #[serde(skip_serializing_if = "Option::is_none")]
416 pub trace_fingerprint: Option<u64>,
417
418 #[serde(skip_serializing_if = "Option::is_none")]
420 pub schedule_hash: Option<u64>,
421
422 #[serde(skip_serializing_if = "Option::is_none")]
424 pub event_hash: Option<u64>,
425
426 #[serde(skip_serializing_if = "Option::is_none")]
428 pub event_count: Option<u64>,
429
430 #[serde(skip_serializing_if = "Option::is_none")]
432 pub steps_total: Option<u64>,
433
434 #[serde(skip_serializing_if = "Option::is_none")]
436 pub artifact_path: Option<String>,
437
438 #[serde(skip_serializing_if = "Option::is_none")]
440 pub repro_command: Option<String>,
441
442 #[serde(skip_serializing_if = "Option::is_none")]
444 pub config_hash: Option<String>,
445
446 #[serde(default, skip_serializing_if = "Vec::is_empty")]
448 pub nondeterminism_notes: Vec<String>,
449}
450
451impl ReplayMetadata {
452 #[must_use]
454 pub fn for_lab(family: ScenarioFamilyId, seed_plan: &SeedPlan) -> Self {
455 let effective_seed = seed_plan.effective_lab_seed();
456 let effective_entropy_seed = seed_plan.effective_entropy_seed(effective_seed);
457 let instance = ExecutionInstanceId::lab(&family.id, effective_seed);
458
459 Self {
460 family,
461 instance,
462 seed_plan: seed_plan.clone(),
463 effective_seed,
464 effective_entropy_seed,
465 trace_fingerprint: None,
466 schedule_hash: None,
467 event_hash: None,
468 event_count: None,
469 steps_total: None,
470 artifact_path: None,
471 repro_command: None,
472 config_hash: None,
473 nondeterminism_notes: Vec::new(),
474 }
475 }
476
477 #[must_use]
479 pub fn for_live(family: ScenarioFamilyId, seed_plan: &SeedPlan) -> Self {
480 let effective_seed = seed_plan.effective_live_seed();
481 let effective_entropy_seed = seed_plan.effective_entropy_seed(effective_seed);
482 let instance = ExecutionInstanceId::live(&family.id, effective_seed);
483
484 Self {
485 family,
486 instance,
487 seed_plan: seed_plan.clone(),
488 effective_seed,
489 effective_entropy_seed,
490 trace_fingerprint: None,
491 schedule_hash: None,
492 event_hash: None,
493 event_count: None,
494 steps_total: None,
495 artifact_path: None,
496 repro_command: None,
497 config_hash: None,
498 nondeterminism_notes: Vec::new(),
499 }
500 }
501
502 #[must_use]
504 pub fn with_lab_report(
505 mut self,
506 trace_fingerprint: u64,
507 event_hash: u64,
508 event_count: u64,
509 schedule_hash: u64,
510 steps_total: u64,
511 ) -> Self {
512 self.trace_fingerprint = Some(trace_fingerprint);
513 self.event_hash = Some(event_hash);
514 self.event_count = Some(event_count);
515 self.schedule_hash = Some(schedule_hash);
516 self.steps_total = Some(steps_total);
517 self
518 }
519
520 #[must_use]
522 pub fn with_repro_command(mut self, cmd: impl Into<String>) -> Self {
523 self.repro_command = Some(cmd.into());
524 self
525 }
526
527 #[must_use]
529 pub fn with_artifact_path(mut self, path: impl Into<String>) -> Self {
530 self.artifact_path = Some(path.into());
531 self
532 }
533
534 #[must_use]
536 pub fn with_nondeterminism_notes(mut self, notes: Vec<String>) -> Self {
537 self.nondeterminism_notes = notes;
538 self
539 }
540
541 #[must_use]
543 pub fn default_repro_command(&self) -> String {
544 format!(
545 "rch exec -- env ASUPERSYNC_SEED=0x{:X} cargo test {} -- --nocapture",
546 self.effective_seed, self.family.id
547 )
548 }
549}
550
551#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
561pub struct SeedLineageRecord {
562 pub seed_lineage_id: String,
564
565 pub canonical_seed: u64,
567
568 pub lab_effective_seed: u64,
570
571 pub live_effective_seed: u64,
573
574 pub lab_seed_mode: SeedMode,
576
577 pub live_seed_mode: SeedMode,
579
580 pub lab_entropy_seed: u64,
582
583 pub live_entropy_seed: u64,
585
586 pub replay_policy: ReplayPolicy,
588
589 pub seeds_match: bool,
591
592 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
594 pub annotations: BTreeMap<String, String>,
595}
596
597impl SeedLineageRecord {
598 #[must_use]
600 pub fn from_plan(plan: &SeedPlan) -> Self {
601 let lab_seed = plan.effective_lab_seed();
602 let live_seed = plan.effective_live_seed();
603 let lab_entropy = plan.effective_entropy_seed(lab_seed);
604 let live_entropy = plan.effective_entropy_seed(live_seed);
605
606 Self {
607 seed_lineage_id: plan.seed_lineage_id.clone(),
608 canonical_seed: plan.canonical_seed,
609 lab_effective_seed: lab_seed,
610 live_effective_seed: live_seed,
611 lab_seed_mode: plan.lab_seed_mode,
612 live_seed_mode: plan.live_seed_mode,
613 lab_entropy_seed: lab_entropy,
614 live_entropy_seed: live_entropy,
615 replay_policy: plan.replay_policy,
616 seeds_match: lab_seed == live_seed,
617 annotations: BTreeMap::new(),
618 }
619 }
620
621 #[must_use]
623 pub fn with_annotation(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
624 self.annotations.insert(key.into(), value.into());
625 self
626 }
627}
628
629pub const DUAL_RUN_SCHEMA_VERSION: &str = "lab-live-scenario-spec-v1";
635
636#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
638pub enum Phase {
639 #[serde(rename = "Phase 1")]
642 Phase1,
643 #[serde(rename = "Phase 2")]
645 Phase2,
646 #[serde(rename = "Phase 3")]
648 Phase3,
649}
650
651impl fmt::Display for Phase {
652 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
653 match self {
654 Self::Phase1 => write!(f, "Phase 1"),
655 Self::Phase2 => write!(f, "Phase 2"),
656 Self::Phase3 => write!(f, "Phase 3"),
657 }
658 }
659}
660
661#[derive(Debug, Clone, Serialize, Deserialize)]
671pub struct DualRunScenarioIdentity {
672 pub schema_version: String,
674
675 pub scenario_id: String,
677
678 pub surface_id: String,
680
681 pub surface_contract_version: String,
683
684 pub description: String,
686
687 pub phase: Phase,
689
690 pub seed_plan: SeedPlan,
692
693 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
695 pub metadata: BTreeMap<String, String>,
696}
697
698impl DualRunScenarioIdentity {
699 #[must_use]
701 pub fn phase1(
702 scenario_id: impl Into<String>,
703 surface_id: impl Into<String>,
704 contract_version: impl Into<String>,
705 description: impl Into<String>,
706 canonical_seed: u64,
707 ) -> Self {
708 let sid = scenario_id.into();
709 Self {
710 schema_version: DUAL_RUN_SCHEMA_VERSION.to_string(),
711 scenario_id: sid.clone(),
712 surface_id: surface_id.into(),
713 surface_contract_version: contract_version.into(),
714 description: description.into(),
715 phase: Phase::Phase1,
716 seed_plan: SeedPlan::inherit(canonical_seed, sid),
717 metadata: BTreeMap::new(),
718 }
719 }
720
721 #[must_use]
723 pub fn family_id(&self) -> ScenarioFamilyId {
724 ScenarioFamilyId::new(
725 &self.scenario_id,
726 &self.surface_id,
727 &self.surface_contract_version,
728 )
729 }
730
731 #[must_use]
733 pub fn lab_replay_metadata(&self) -> ReplayMetadata {
734 ReplayMetadata::for_lab(self.family_id(), &self.seed_plan)
735 }
736
737 #[must_use]
739 pub fn live_replay_metadata(&self) -> ReplayMetadata {
740 ReplayMetadata::for_live(self.family_id(), &self.seed_plan)
741 }
742
743 #[must_use]
745 pub fn seed_lineage(&self) -> SeedLineageRecord {
746 SeedLineageRecord::from_plan(&self.seed_plan)
747 }
748
749 #[must_use]
751 pub fn to_lab_config(&self) -> LabConfig {
752 self.seed_plan.to_lab_config()
753 }
754
755 #[must_use]
757 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
758 self.metadata.insert(key.into(), value.into());
759 self
760 }
761
762 #[must_use]
764 pub fn with_seed_plan(mut self, plan: SeedPlan) -> Self {
765 self.seed_plan = plan;
766 self
767 }
768}
769
770pub const NORMALIZED_OBSERVABLE_SCHEMA_VERSION: &str = "lab-live-normalized-observable-v1";
776
777#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
779#[serde(rename_all = "snake_case")]
780pub enum OutcomeClass {
781 Ok,
783 Err,
785 Cancelled,
787 Panicked,
789}
790
791impl fmt::Display for OutcomeClass {
792 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
793 match self {
794 Self::Ok => write!(f, "ok"),
795 Self::Err => write!(f, "err"),
796 Self::Cancelled => write!(f, "cancelled"),
797 Self::Panicked => write!(f, "panicked"),
798 }
799 }
800}
801
802#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
804#[serde(rename_all = "snake_case")]
805#[allow(missing_docs)]
806pub enum CancelTerminalPhase {
807 NotCancelled,
808 CancelRequested,
809 Cancelling,
810 Finalizing,
811 Completed,
812}
813
814#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
816#[serde(rename_all = "snake_case")]
817pub enum DrainStatus {
818 NotApplicable,
820 Complete,
822 Incomplete,
824}
825
826#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
828#[serde(rename_all = "snake_case")]
829pub enum RegionState {
830 Open,
832 Closing,
834 Draining,
836 Finalizing,
838 Closed,
840}
841
842#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
844#[serde(rename_all = "snake_case")]
845pub enum CounterTolerance {
846 Exact,
848 AtLeast,
850 AtMost,
852 Unsupported,
854}
855
856#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
858#[allow(missing_docs)]
859pub struct TerminalOutcome {
860 pub class: OutcomeClass,
861 pub severity: OutcomeClass,
862 #[serde(skip_serializing_if = "Option::is_none")]
863 pub surface_result: Option<String>,
864 #[serde(skip_serializing_if = "Option::is_none")]
865 pub error_class: Option<String>,
866 #[serde(skip_serializing_if = "Option::is_none")]
867 pub cancel_reason_class: Option<String>,
868 #[serde(skip_serializing_if = "Option::is_none")]
869 pub panic_class: Option<String>,
870}
871
872impl TerminalOutcome {
873 #[must_use]
875 pub fn ok() -> Self {
876 Self {
877 class: OutcomeClass::Ok,
878 severity: OutcomeClass::Ok,
879 surface_result: None,
880 error_class: None,
881 cancel_reason_class: None,
882 panic_class: None,
883 }
884 }
885
886 #[must_use]
888 pub fn cancelled(reason_class: impl Into<String>) -> Self {
889 Self {
890 class: OutcomeClass::Cancelled,
891 severity: OutcomeClass::Cancelled,
892 surface_result: None,
893 error_class: None,
894 cancel_reason_class: Some(reason_class.into()),
895 panic_class: None,
896 }
897 }
898
899 #[must_use]
901 pub fn err(error_class: impl Into<String>) -> Self {
902 Self {
903 class: OutcomeClass::Err,
904 severity: OutcomeClass::Err,
905 surface_result: None,
906 error_class: Some(error_class.into()),
907 cancel_reason_class: None,
908 panic_class: None,
909 }
910 }
911}
912
913#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
915#[allow(clippy::struct_excessive_bools)]
916#[allow(missing_docs)]
917pub struct CancellationRecord {
918 pub requested: bool,
919 pub acknowledged: bool,
920 pub cleanup_completed: bool,
921 pub finalization_completed: bool,
922 pub terminal_phase: CancelTerminalPhase,
923 #[serde(skip_serializing_if = "Option::is_none")]
924 pub checkpoint_observed: Option<bool>,
925}
926
927impl CancellationRecord {
928 #[must_use]
930 pub fn none() -> Self {
931 Self {
932 requested: false,
933 acknowledged: false,
934 cleanup_completed: false,
935 finalization_completed: false,
936 terminal_phase: CancelTerminalPhase::NotCancelled,
937 checkpoint_observed: None,
938 }
939 }
940
941 #[must_use]
943 pub fn completed() -> Self {
944 Self {
945 requested: true,
946 acknowledged: true,
947 cleanup_completed: true,
948 finalization_completed: true,
949 terminal_phase: CancelTerminalPhase::Completed,
950 checkpoint_observed: Some(true),
951 }
952 }
953}
954
955#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
957#[allow(missing_docs)]
958pub struct LoserDrainRecord {
959 pub applicable: bool,
960 pub expected_losers: u32,
961 pub drained_losers: u32,
962 pub status: DrainStatus,
963 #[serde(skip_serializing_if = "Option::is_none")]
964 pub evidence: Option<String>,
965}
966
967impl LoserDrainRecord {
968 #[must_use]
970 pub fn not_applicable() -> Self {
971 Self {
972 applicable: false,
973 expected_losers: 0,
974 drained_losers: 0,
975 status: DrainStatus::NotApplicable,
976 evidence: None,
977 }
978 }
979
980 #[must_use]
982 pub fn complete(expected: u32) -> Self {
983 Self {
984 applicable: true,
985 expected_losers: expected,
986 drained_losers: expected,
987 status: DrainStatus::Complete,
988 evidence: None,
989 }
990 }
991}
992
993#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
995#[allow(missing_docs)]
996pub struct RegionCloseRecord {
997 pub root_state: RegionState,
998 pub quiescent: bool,
999 pub live_children: u32,
1000 pub finalizers_pending: u32,
1001 pub close_completed: bool,
1002}
1003
1004impl RegionCloseRecord {
1005 #[must_use]
1007 pub fn quiescent() -> Self {
1008 Self {
1009 root_state: RegionState::Closed,
1010 quiescent: true,
1011 live_children: 0,
1012 finalizers_pending: 0,
1013 close_completed: true,
1014 }
1015 }
1016}
1017
1018#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1020#[allow(missing_docs)]
1021pub struct ObligationBalanceRecord {
1022 pub reserved: u32,
1023 pub committed: u32,
1024 pub aborted: u32,
1025 pub leaked: u32,
1026 pub unresolved: u32,
1027 pub balanced: bool,
1028}
1029
1030impl ObligationBalanceRecord {
1031 #[must_use]
1033 pub fn balanced(reserved: u32, committed: u32, aborted: u32) -> Self {
1034 Self {
1035 reserved,
1036 committed,
1037 aborted,
1038 leaked: 0,
1039 unresolved: 0,
1040 balanced: true,
1041 }
1042 }
1043
1044 #[must_use]
1046 pub fn zero() -> Self {
1047 Self::balanced(0, 0, 0)
1048 }
1049
1050 #[must_use]
1052 pub fn recompute(mut self) -> Self {
1053 let terminal = self
1054 .committed
1055 .saturating_add(self.aborted)
1056 .saturating_add(self.leaked);
1057 self.unresolved = self.reserved.saturating_sub(terminal);
1058 self.balanced = self.leaked == 0 && self.unresolved == 0;
1059 self
1060 }
1061}
1062
1063#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1065#[allow(missing_docs)]
1066pub struct ResourceSurfaceRecord {
1067 pub contract_scope: String,
1068 #[serde(default)]
1069 pub counters: BTreeMap<String, i64>,
1070 #[serde(default)]
1071 pub tolerances: BTreeMap<String, CounterTolerance>,
1072}
1073
1074impl ResourceSurfaceRecord {
1075 #[must_use]
1077 pub fn empty(scope: impl Into<String>) -> Self {
1078 Self {
1079 contract_scope: scope.into(),
1080 counters: BTreeMap::new(),
1081 tolerances: BTreeMap::new(),
1082 }
1083 }
1084
1085 #[must_use]
1087 pub fn with_counter(mut self, name: impl Into<String>, value: i64) -> Self {
1088 let n = name.into();
1089 self.counters.insert(n.clone(), value);
1090 self.tolerances.insert(n, CounterTolerance::Exact);
1091 self
1092 }
1093
1094 #[must_use]
1096 pub fn with_counter_tolerance(
1097 mut self,
1098 name: impl Into<String>,
1099 value: i64,
1100 tolerance: CounterTolerance,
1101 ) -> Self {
1102 let n = name.into();
1103 self.counters.insert(n.clone(), value);
1104 self.tolerances.insert(n, tolerance);
1105 self
1106 }
1107}
1108
1109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1111#[allow(missing_docs)]
1112pub struct NormalizedSemantics {
1113 pub terminal_outcome: TerminalOutcome,
1114 pub cancellation: CancellationRecord,
1115 pub loser_drain: LoserDrainRecord,
1116 pub region_close: RegionCloseRecord,
1117 pub obligation_balance: ObligationBalanceRecord,
1118 pub resource_surface: ResourceSurfaceRecord,
1119}
1120
1121#[derive(Debug, Clone, Serialize, Deserialize)]
1123#[allow(missing_docs)]
1124pub struct NormalizedObservable {
1125 pub schema_version: String,
1126 pub scenario_id: String,
1127 pub surface_id: String,
1128 pub surface_contract_version: String,
1129 pub runtime_kind: RuntimeKind,
1130 pub semantics: NormalizedSemantics,
1131 pub provenance: ReplayMetadata,
1132}
1133
1134impl NormalizedObservable {
1135 #[must_use]
1137 pub fn new(
1138 identity: &DualRunScenarioIdentity,
1139 runtime_kind: RuntimeKind,
1140 semantics: NormalizedSemantics,
1141 provenance: ReplayMetadata,
1142 ) -> Self {
1143 Self {
1144 schema_version: NORMALIZED_OBSERVABLE_SCHEMA_VERSION.to_string(),
1145 scenario_id: identity.scenario_id.clone(),
1146 surface_id: identity.surface_id.clone(),
1147 surface_contract_version: identity.surface_contract_version.clone(),
1148 runtime_kind,
1149 semantics,
1150 provenance,
1151 }
1152 }
1153}
1154
1155#[derive(Debug, Clone, Serialize, Deserialize)]
1161pub struct SemanticMismatch {
1162 pub field: String,
1164 pub description: String,
1166 pub lab_value: String,
1168 pub live_value: String,
1170}
1171
1172impl fmt::Display for SemanticMismatch {
1173 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1174 write!(
1175 f,
1176 "{}: {} (lab={}, live={})",
1177 self.field, self.description, self.lab_value, self.live_value
1178 )
1179 }
1180}
1181
1182#[derive(Debug, Clone, Serialize, Deserialize)]
1184pub struct ComparisonVerdict {
1185 pub scenario_id: String,
1187 pub surface_id: String,
1189 pub passed: bool,
1191 pub mismatches: Vec<SemanticMismatch>,
1193 pub seed_lineage: SeedLineageRecord,
1195}
1196
1197impl ComparisonVerdict {
1198 #[must_use]
1200 pub fn is_equivalent(&self) -> bool {
1201 self.passed
1202 }
1203
1204 #[must_use]
1206 pub fn summary(&self) -> String {
1207 if self.passed {
1208 format!(
1209 "PASS: {} on {} (seed lineage: {})",
1210 self.scenario_id, self.surface_id, self.seed_lineage.seed_lineage_id
1211 )
1212 } else {
1213 let mismatch_list: Vec<String> =
1214 self.mismatches.iter().map(ToString::to_string).collect();
1215 format!(
1216 "FAIL: {} on {} — {} mismatch(es):\n {}",
1217 self.scenario_id,
1218 self.surface_id,
1219 self.mismatches.len(),
1220 mismatch_list.join("\n ")
1221 )
1222 }
1223 }
1224
1225 #[must_use]
1227 pub fn summary_with_manifests(
1228 &self,
1229 lab_manifest: Option<&CaptureManifest>,
1230 live_manifest: Option<&CaptureManifest>,
1231 ) -> String {
1232 if self.passed {
1233 return self.summary();
1234 }
1235
1236 let mismatch_list: Vec<String> = self
1237 .mismatches
1238 .iter()
1239 .map(|mismatch| {
1240 let mut line = mismatch.to_string();
1241 let mut capture_notes = Vec::new();
1242 if let Some(lab_capture) = lab_manifest
1243 .and_then(|manifest| manifest.describe_field_capture(&mismatch.field))
1244 {
1245 capture_notes.push(format!("lab_capture={lab_capture}"));
1246 }
1247 if let Some(live_capture) = live_manifest
1248 .and_then(|manifest| manifest.describe_field_capture(&mismatch.field))
1249 {
1250 capture_notes.push(format!("live_capture={live_capture}"));
1251 }
1252 if !capture_notes.is_empty() {
1253 line.push_str(" [");
1254 line.push_str(&capture_notes.join("; "));
1255 line.push(']');
1256 }
1257 line
1258 })
1259 .collect();
1260
1261 format!(
1262 "FAIL: {} on {} — {} mismatch(es):\n {}",
1263 self.scenario_id,
1264 self.surface_id,
1265 self.mismatches.len(),
1266 mismatch_list.join("\n ")
1267 )
1268 }
1269}
1270
1271impl fmt::Display for ComparisonVerdict {
1272 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1273 write!(f, "{}", self.summary())
1274 }
1275}
1276
1277#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1279#[serde(rename_all = "snake_case")]
1280pub enum ProvisionalDivergenceClass {
1281 Pass,
1283 UnsupportedSurface,
1285 ArtifactSchemaViolation,
1287 InsufficientObservability,
1289 SchedulerNoiseSuspected,
1291 SemanticMismatchAdmittedSurface,
1293 HardContractBreak,
1295}
1296
1297impl fmt::Display for ProvisionalDivergenceClass {
1298 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1299 match self {
1300 Self::Pass => write!(f, "pass"),
1301 Self::UnsupportedSurface => write!(f, "unsupported_surface"),
1302 Self::ArtifactSchemaViolation => write!(f, "artifact_schema_violation"),
1303 Self::InsufficientObservability => write!(f, "insufficient_observability"),
1304 Self::SchedulerNoiseSuspected => write!(f, "scheduler_noise_suspected"),
1305 Self::SemanticMismatchAdmittedSurface => {
1306 write!(f, "semantic_mismatch_admitted_surface")
1307 }
1308 Self::HardContractBreak => write!(f, "hard_contract_break"),
1309 }
1310 }
1311}
1312
1313#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1315#[serde(rename_all = "snake_case")]
1316pub enum FinalDivergenceClass {
1317 RuntimeSemanticBug,
1318 LabModelOrMappingBug,
1319 IrreproducibleDivergence,
1320 UnsupportedSurface,
1321 ArtifactSchemaViolation,
1322 InsufficientObservability,
1323 SchedulerNoiseSuspected,
1324}
1325
1326impl fmt::Display for FinalDivergenceClass {
1327 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1328 match self {
1329 Self::RuntimeSemanticBug => write!(f, "runtime_semantic_bug"),
1330 Self::LabModelOrMappingBug => write!(f, "lab_model_or_mapping_bug"),
1331 Self::IrreproducibleDivergence => write!(f, "irreproducible_divergence"),
1332 Self::UnsupportedSurface => write!(f, "unsupported_surface"),
1333 Self::ArtifactSchemaViolation => write!(f, "artifact_schema_violation"),
1334 Self::InsufficientObservability => write!(f, "insufficient_observability"),
1335 Self::SchedulerNoiseSuspected => write!(f, "scheduler_noise_suspected"),
1336 }
1337 }
1338}
1339
1340#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1342#[serde(rename_all = "snake_case")]
1343pub enum TimePolicyClass {
1344 NotApplicable,
1345 ProvenanceOnlyTime,
1346 SchedulerNoiseSignal,
1347 QualifiedTime,
1348 UnsupportedTimeSurface,
1349 SemanticTime,
1350 PolicyViolation,
1351}
1352
1353impl fmt::Display for TimePolicyClass {
1354 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1355 match self {
1356 Self::NotApplicable => write!(f, "not_applicable"),
1357 Self::ProvenanceOnlyTime => write!(f, "provenance_only_time"),
1358 Self::SchedulerNoiseSignal => write!(f, "scheduler_noise_signal"),
1359 Self::QualifiedTime => write!(f, "qualified_time"),
1360 Self::UnsupportedTimeSurface => write!(f, "unsupported_time_surface"),
1361 Self::SemanticTime => write!(f, "semantic_time"),
1362 Self::PolicyViolation => write!(f, "policy_violation"),
1363 }
1364 }
1365}
1366
1367#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1369#[serde(rename_all = "snake_case")]
1370pub enum SchedulerNoiseClass {
1371 None,
1372 NondeterminismNotesOnly,
1373 ScheduleHashDrift,
1374 EventHashDrift,
1375 EventCountDrift,
1376 ProvenanceDrift,
1377}
1378
1379impl fmt::Display for SchedulerNoiseClass {
1380 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1381 match self {
1382 Self::None => write!(f, "none"),
1383 Self::NondeterminismNotesOnly => write!(f, "nondeterminism_notes_only"),
1384 Self::ScheduleHashDrift => write!(f, "schedule_hash_drift"),
1385 Self::EventHashDrift => write!(f, "event_hash_drift"),
1386 Self::EventCountDrift => write!(f, "event_count_drift"),
1387 Self::ProvenanceDrift => write!(f, "provenance_drift"),
1388 }
1389 }
1390}
1391
1392#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1394#[serde(tag = "kind", rename_all = "snake_case")]
1395pub enum RerunDecision {
1396 None,
1397 LiveConfirmations { additional_runs: u8 },
1398 DeterministicLabReplayAndLiveConfirmations { additional_live_runs: u8 },
1399 ConfirmationIfRicherInstrumentationEnabled { additional_runs: u8 },
1400}
1401
1402impl fmt::Display for RerunDecision {
1403 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1404 match self {
1405 Self::None => write!(f, "none"),
1406 Self::LiveConfirmations { additional_runs } => {
1407 write!(f, "live_confirmations(+{additional_runs})")
1408 }
1409 Self::DeterministicLabReplayAndLiveConfirmations {
1410 additional_live_runs,
1411 } => write!(
1412 f,
1413 "deterministic_lab_replay_and_live_confirmations(+{additional_live_runs} live)"
1414 ),
1415 Self::ConfirmationIfRicherInstrumentationEnabled { additional_runs } => write!(
1416 f,
1417 "confirmation_if_richer_instrumentation_enabled(+{additional_runs})"
1418 ),
1419 }
1420 }
1421}
1422
1423#[derive(Debug, Clone, Serialize, Deserialize)]
1425pub struct DifferentialPolicyOutcome {
1426 pub provisional_class: ProvisionalDivergenceClass,
1428 pub rerun_decision: RerunDecision,
1430 #[serde(skip_serializing_if = "Option::is_none")]
1432 pub suggested_final_class: Option<FinalDivergenceClass>,
1433 pub time_policy_class: TimePolicyClass,
1435 pub scheduler_noise_class: SchedulerNoiseClass,
1437 #[serde(skip_serializing_if = "Option::is_none")]
1439 pub suppression_reason: Option<String>,
1440 pub explanation: String,
1442}
1443
1444impl DifferentialPolicyOutcome {
1445 #[must_use]
1446 pub fn summary(&self) -> String {
1447 let mut parts = vec![
1448 format!("provisional_class={}", self.provisional_class),
1449 format!("rerun_decision={}", self.rerun_decision),
1450 format!("time_policy_class={}", self.time_policy_class),
1451 format!("scheduler_noise_class={}", self.scheduler_noise_class),
1452 ];
1453 if let Some(final_class) = self.suggested_final_class {
1454 parts.push(format!("suggested_final_class={final_class}"));
1455 }
1456 if let Some(reason) = &self.suppression_reason {
1457 parts.push(format!("suppression_reason={reason}"));
1458 }
1459 parts.push(self.explanation.clone());
1460 parts.join("; ")
1461 }
1462}
1463
1464impl fmt::Display for DifferentialPolicyOutcome {
1465 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1466 write!(f, "{}", self.summary())
1467 }
1468}
1469
1470#[must_use]
1475pub fn compare_observables(
1476 lab: &NormalizedObservable,
1477 live: &NormalizedObservable,
1478 seed_lineage: SeedLineageRecord,
1479) -> ComparisonVerdict {
1480 let mut mismatches = Vec::new();
1481
1482 if lab.schema_version != live.schema_version {
1484 mismatches.push(SemanticMismatch {
1485 field: "schema_version".to_string(),
1486 description: "Schema version mismatch".to_string(),
1487 lab_value: lab.schema_version.clone(),
1488 live_value: live.schema_version.clone(),
1489 });
1490 }
1491
1492 if lab.scenario_id != live.scenario_id {
1494 mismatches.push(SemanticMismatch {
1495 field: "scenario_id".to_string(),
1496 description: "Scenario ID mismatch".to_string(),
1497 lab_value: lab.scenario_id.clone(),
1498 live_value: live.scenario_id.clone(),
1499 });
1500 }
1501 if lab.surface_id != live.surface_id {
1502 mismatches.push(SemanticMismatch {
1503 field: "surface_id".to_string(),
1504 description: "Surface ID mismatch".to_string(),
1505 lab_value: lab.surface_id.clone(),
1506 live_value: live.surface_id.clone(),
1507 });
1508 }
1509 if lab.surface_contract_version != live.surface_contract_version {
1510 mismatches.push(SemanticMismatch {
1511 field: "surface_contract_version".to_string(),
1512 description: "Surface contract version mismatch".to_string(),
1513 lab_value: lab.surface_contract_version.clone(),
1514 live_value: live.surface_contract_version.clone(),
1515 });
1516 }
1517
1518 compare_terminal_outcome(
1520 &lab.semantics.terminal_outcome,
1521 &live.semantics.terminal_outcome,
1522 &mut mismatches,
1523 );
1524
1525 compare_cancellation(
1527 &lab.semantics.cancellation,
1528 &live.semantics.cancellation,
1529 &mut mismatches,
1530 );
1531
1532 compare_loser_drain(
1534 &lab.semantics.loser_drain,
1535 &live.semantics.loser_drain,
1536 &mut mismatches,
1537 );
1538
1539 compare_region_close(
1541 &lab.semantics.region_close,
1542 &live.semantics.region_close,
1543 &mut mismatches,
1544 );
1545
1546 compare_obligation_balance(
1548 &lab.semantics.obligation_balance,
1549 &live.semantics.obligation_balance,
1550 &mut mismatches,
1551 );
1552
1553 compare_resource_surface(
1555 &lab.semantics.resource_surface,
1556 &live.semantics.resource_surface,
1557 &mut mismatches,
1558 );
1559
1560 ComparisonVerdict {
1561 scenario_id: lab.scenario_id.clone(),
1562 surface_id: lab.surface_id.clone(),
1563 passed: mismatches.is_empty(),
1564 mismatches,
1565 seed_lineage,
1566 }
1567}
1568
1569fn compare_terminal_outcome(
1570 lab: &TerminalOutcome,
1571 live: &TerminalOutcome,
1572 mismatches: &mut Vec<SemanticMismatch>,
1573) {
1574 if lab.class != live.class {
1575 mismatches.push(SemanticMismatch {
1576 field: "semantics.terminal_outcome.class".to_string(),
1577 description: "Terminal outcome class mismatch".to_string(),
1578 lab_value: format!("{}", lab.class),
1579 live_value: format!("{}", live.class),
1580 });
1581 }
1582 if lab.severity != live.severity {
1583 mismatches.push(SemanticMismatch {
1584 field: "semantics.terminal_outcome.severity".to_string(),
1585 description: "Terminal outcome severity mismatch".to_string(),
1586 lab_value: format!("{}", lab.severity),
1587 live_value: format!("{}", live.severity),
1588 });
1589 }
1590 if lab.surface_result != live.surface_result {
1591 mismatches.push(SemanticMismatch {
1592 field: "semantics.terminal_outcome.surface_result".to_string(),
1593 description: "Surface result mismatch".to_string(),
1594 lab_value: format!("{:?}", lab.surface_result),
1595 live_value: format!("{:?}", live.surface_result),
1596 });
1597 }
1598 if lab.error_class != live.error_class {
1599 mismatches.push(SemanticMismatch {
1600 field: "semantics.terminal_outcome.error_class".to_string(),
1601 description: "Error class mismatch".to_string(),
1602 lab_value: format!("{:?}", lab.error_class),
1603 live_value: format!("{:?}", live.error_class),
1604 });
1605 }
1606 if lab.cancel_reason_class != live.cancel_reason_class {
1607 mismatches.push(SemanticMismatch {
1608 field: "semantics.terminal_outcome.cancel_reason_class".to_string(),
1609 description: "Cancel reason class mismatch".to_string(),
1610 lab_value: format!("{:?}", lab.cancel_reason_class),
1611 live_value: format!("{:?}", live.cancel_reason_class),
1612 });
1613 }
1614 if lab.panic_class != live.panic_class {
1615 mismatches.push(SemanticMismatch {
1616 field: "semantics.terminal_outcome.panic_class".to_string(),
1617 description: "Panic class mismatch".to_string(),
1618 lab_value: format!("{:?}", lab.panic_class),
1619 live_value: format!("{:?}", live.panic_class),
1620 });
1621 }
1622}
1623
1624fn compare_cancellation(
1625 lab: &CancellationRecord,
1626 live: &CancellationRecord,
1627 mismatches: &mut Vec<SemanticMismatch>,
1628) {
1629 let fields = [
1630 ("requested", lab.requested, live.requested),
1631 ("acknowledged", lab.acknowledged, live.acknowledged),
1632 (
1633 "cleanup_completed",
1634 lab.cleanup_completed,
1635 live.cleanup_completed,
1636 ),
1637 (
1638 "finalization_completed",
1639 lab.finalization_completed,
1640 live.finalization_completed,
1641 ),
1642 ];
1643 for (name, lab_val, live_val) in fields {
1644 if lab_val != live_val {
1645 mismatches.push(SemanticMismatch {
1646 field: format!("semantics.cancellation.{name}"),
1647 description: format!("Cancellation {name} mismatch"),
1648 lab_value: format!("{lab_val}"),
1649 live_value: format!("{live_val}"),
1650 });
1651 }
1652 }
1653 if lab.terminal_phase != live.terminal_phase {
1654 mismatches.push(SemanticMismatch {
1655 field: "semantics.cancellation.terminal_phase".to_string(),
1656 description: "Cancellation terminal phase mismatch".to_string(),
1657 lab_value: format!("{:?}", lab.terminal_phase),
1658 live_value: format!("{:?}", live.terminal_phase),
1659 });
1660 }
1661 if let (Some(lab_cp), Some(live_cp)) = (lab.checkpoint_observed, live.checkpoint_observed) {
1663 if lab_cp != live_cp {
1664 mismatches.push(SemanticMismatch {
1665 field: "semantics.cancellation.checkpoint_observed".to_string(),
1666 description: "Checkpoint observed mismatch".to_string(),
1667 lab_value: format!("{lab_cp}"),
1668 live_value: format!("{live_cp}"),
1669 });
1670 }
1671 }
1672}
1673
1674fn compare_loser_drain(
1675 lab: &LoserDrainRecord,
1676 live: &LoserDrainRecord,
1677 mismatches: &mut Vec<SemanticMismatch>,
1678) {
1679 if lab.status != live.status {
1680 mismatches.push(SemanticMismatch {
1681 field: "semantics.loser_drain.status".to_string(),
1682 description: "Loser drain status mismatch".to_string(),
1683 lab_value: format!("{:?}", lab.status),
1684 live_value: format!("{:?}", live.status),
1685 });
1686 }
1687 if lab.applicable != live.applicable {
1688 mismatches.push(SemanticMismatch {
1689 field: "semantics.loser_drain.applicable".to_string(),
1690 description: "Loser drain applicability mismatch".to_string(),
1691 lab_value: format!("{}", lab.applicable),
1692 live_value: format!("{}", live.applicable),
1693 });
1694 }
1695 let counts_unknown = loser_drain_counts_unknown(lab) || loser_drain_counts_unknown(live);
1696 if !counts_unknown && lab.expected_losers != live.expected_losers {
1697 mismatches.push(SemanticMismatch {
1698 field: "semantics.loser_drain.expected_losers".to_string(),
1699 description: "Expected losers count mismatch".to_string(),
1700 lab_value: format!("{}", lab.expected_losers),
1701 live_value: format!("{}", live.expected_losers),
1702 });
1703 }
1704 if !counts_unknown && lab.drained_losers != live.drained_losers {
1705 mismatches.push(SemanticMismatch {
1706 field: "semantics.loser_drain.drained_losers".to_string(),
1707 description: "Drained losers count mismatch".to_string(),
1708 lab_value: format!("{}", lab.drained_losers),
1709 live_value: format!("{}", live.drained_losers),
1710 });
1711 }
1712}
1713
1714fn compare_region_close(
1715 lab: &RegionCloseRecord,
1716 live: &RegionCloseRecord,
1717 mismatches: &mut Vec<SemanticMismatch>,
1718) {
1719 if lab.quiescent && live.quiescent && lab.root_state != live.root_state {
1724 mismatches.push(SemanticMismatch {
1725 field: "semantics.region_close.root_state".to_string(),
1726 description: "Region root state mismatch".to_string(),
1727 lab_value: format!("{:?}", lab.root_state),
1728 live_value: format!("{:?}", live.root_state),
1729 });
1730 }
1731 if lab.quiescent != live.quiescent {
1732 mismatches.push(SemanticMismatch {
1733 field: "semantics.region_close.quiescent".to_string(),
1734 description: "Region quiescence mismatch".to_string(),
1735 lab_value: format!("{}", lab.quiescent),
1736 live_value: format!("{}", live.quiescent),
1737 });
1738 }
1739 if lab.close_completed != live.close_completed {
1740 mismatches.push(SemanticMismatch {
1741 field: "semantics.region_close.close_completed".to_string(),
1742 description: "Region close completed mismatch".to_string(),
1743 lab_value: format!("{}", lab.close_completed),
1744 live_value: format!("{}", live.close_completed),
1745 });
1746 }
1747 let counts_unknown = region_close_counts_unknown(lab) || region_close_counts_unknown(live);
1748 if !counts_unknown && lab.live_children != live.live_children {
1749 mismatches.push(SemanticMismatch {
1750 field: "semantics.region_close.live_children".to_string(),
1751 description: "Region live child count mismatch".to_string(),
1752 lab_value: format!("{}", lab.live_children),
1753 live_value: format!("{}", live.live_children),
1754 });
1755 }
1756 if !counts_unknown && lab.finalizers_pending != live.finalizers_pending {
1757 mismatches.push(SemanticMismatch {
1758 field: "semantics.region_close.finalizers_pending".to_string(),
1759 description: "Region finalizers pending mismatch".to_string(),
1760 lab_value: format!("{}", lab.finalizers_pending),
1761 live_value: format!("{}", live.finalizers_pending),
1762 });
1763 }
1764}
1765
1766fn loser_drain_counts_unknown(record: &LoserDrainRecord) -> bool {
1767 record.applicable
1768 && record.expected_losers == 0
1769 && record.drained_losers == 0
1770 && record
1771 .evidence
1772 .as_deref()
1773 .is_some_and(|source| source.starts_with("oracle.loser_drain."))
1774}
1775
1776fn region_close_counts_unknown(record: &RegionCloseRecord) -> bool {
1777 !record.quiescent
1778 && !record.close_completed
1779 && record.root_state == RegionState::Closing
1780 && record.live_children == 0
1781 && record.finalizers_pending == 0
1782}
1783
1784fn compare_obligation_balance(
1785 lab: &ObligationBalanceRecord,
1786 live: &ObligationBalanceRecord,
1787 mismatches: &mut Vec<SemanticMismatch>,
1788) {
1789 if lab.balanced != live.balanced {
1790 mismatches.push(SemanticMismatch {
1791 field: "semantics.obligation_balance.balanced".to_string(),
1792 description: "Obligation balance mismatch".to_string(),
1793 lab_value: format!("{}", lab.balanced),
1794 live_value: format!("{}", live.balanced),
1795 });
1796 }
1797 if lab.leaked != live.leaked {
1798 mismatches.push(SemanticMismatch {
1799 field: "semantics.obligation_balance.leaked".to_string(),
1800 description: "Leaked obligation count mismatch".to_string(),
1801 lab_value: format!("{}", lab.leaked),
1802 live_value: format!("{}", live.leaked),
1803 });
1804 }
1805 if lab.unresolved != live.unresolved {
1806 mismatches.push(SemanticMismatch {
1807 field: "semantics.obligation_balance.unresolved".to_string(),
1808 description: "Unresolved obligation count mismatch".to_string(),
1809 lab_value: format!("{}", lab.unresolved),
1810 live_value: format!("{}", live.unresolved),
1811 });
1812 }
1813 if lab.reserved != live.reserved {
1814 mismatches.push(SemanticMismatch {
1815 field: "semantics.obligation_balance.reserved".to_string(),
1816 description: "Reserved obligation count mismatch".to_string(),
1817 lab_value: format!("{}", lab.reserved),
1818 live_value: format!("{}", live.reserved),
1819 });
1820 }
1821 if lab.committed != live.committed {
1822 mismatches.push(SemanticMismatch {
1823 field: "semantics.obligation_balance.committed".to_string(),
1824 description: "Committed obligation count mismatch".to_string(),
1825 lab_value: format!("{}", lab.committed),
1826 live_value: format!("{}", live.committed),
1827 });
1828 }
1829 if lab.aborted != live.aborted {
1830 mismatches.push(SemanticMismatch {
1831 field: "semantics.obligation_balance.aborted".to_string(),
1832 description: "Aborted obligation count mismatch".to_string(),
1833 lab_value: format!("{}", lab.aborted),
1834 live_value: format!("{}", live.aborted),
1835 });
1836 }
1837}
1838
1839fn compare_resource_surface(
1840 lab: &ResourceSurfaceRecord,
1841 live: &ResourceSurfaceRecord,
1842 mismatches: &mut Vec<SemanticMismatch>,
1843) {
1844 if lab.contract_scope != live.contract_scope {
1845 mismatches.push(SemanticMismatch {
1846 field: "semantics.resource_surface.contract_scope".to_string(),
1847 description: "Resource surface contract scope mismatch".to_string(),
1848 lab_value: lab.contract_scope.clone(),
1849 live_value: live.contract_scope.clone(),
1850 });
1851 return; }
1853
1854 for (name, &lab_val) in &lab.counters {
1856 let Some(&live_val) = live.counters.get(name) else {
1857 mismatches.push(SemanticMismatch {
1858 field: format!("semantics.resource_surface.counters.{name}"),
1859 description: format!("Counter '{name}' missing in live observable"),
1860 lab_value: format!("{lab_val}"),
1861 live_value: "absent".to_string(),
1862 });
1863 continue;
1864 };
1865
1866 let lab_tolerance = lab
1867 .tolerances
1868 .get(name)
1869 .copied()
1870 .unwrap_or(CounterTolerance::Exact);
1871 let live_tolerance = live
1872 .tolerances
1873 .get(name)
1874 .copied()
1875 .unwrap_or(CounterTolerance::Exact);
1876
1877 if lab_tolerance != live_tolerance {
1878 mismatches.push(SemanticMismatch {
1879 field: format!("semantics.resource_surface.tolerances.{name}"),
1880 description: format!("Counter '{name}' tolerance mismatch"),
1881 lab_value: format!("{lab_tolerance:?}"),
1882 live_value: format!("{live_tolerance:?}"),
1883 });
1884 }
1885
1886 let mismatch = match lab_tolerance {
1887 CounterTolerance::Exact => lab_val != live_val,
1888 CounterTolerance::AtLeast => live_val < lab_val,
1889 CounterTolerance::AtMost => live_val > lab_val,
1890 CounterTolerance::Unsupported => false,
1891 };
1892
1893 if mismatch {
1894 mismatches.push(SemanticMismatch {
1895 field: format!("semantics.resource_surface.counters.{name}"),
1896 description: format!("Counter '{name}' mismatch (tolerance: {lab_tolerance:?})"),
1897 lab_value: format!("{lab_val}"),
1898 live_value: format!("{live_val}"),
1899 });
1900 }
1901 }
1902
1903 for name in live.counters.keys() {
1905 if !lab.counters.contains_key(name) {
1906 let live_val = live.counters[name];
1907 mismatches.push(SemanticMismatch {
1908 field: format!("semantics.resource_surface.counters.{name}"),
1909 description: format!("Counter '{name}' present in live but not in lab"),
1910 lab_value: "absent".to_string(),
1911 live_value: format!("{live_val}"),
1912 });
1913 }
1914 }
1915}
1916
1917fn classify_scheduler_noise(
1918 lab: &NormalizedObservable,
1919 live: &NormalizedObservable,
1920) -> SchedulerNoiseClass {
1921 if let (Some(lab_hash), Some(live_hash)) =
1922 (lab.provenance.schedule_hash, live.provenance.schedule_hash)
1923 {
1924 if lab_hash != live_hash {
1925 return SchedulerNoiseClass::ScheduleHashDrift;
1926 }
1927 }
1928 if let (Some(lab_hash), Some(live_hash)) =
1929 (lab.provenance.event_hash, live.provenance.event_hash)
1930 {
1931 if lab_hash != live_hash {
1932 return SchedulerNoiseClass::EventHashDrift;
1933 }
1934 }
1935 if let (Some(lab_count), Some(live_count)) =
1936 (lab.provenance.event_count, live.provenance.event_count)
1937 {
1938 if lab_count != live_count {
1939 return SchedulerNoiseClass::EventCountDrift;
1940 }
1941 }
1942 if lab.provenance.artifact_path != live.provenance.artifact_path
1943 || lab.provenance.config_hash != live.provenance.config_hash
1944 {
1945 return SchedulerNoiseClass::ProvenanceDrift;
1946 }
1947 if !live.provenance.nondeterminism_notes.is_empty() {
1948 return SchedulerNoiseClass::NondeterminismNotesOnly;
1949 }
1950 SchedulerNoiseClass::None
1951}
1952
1953fn classify_time_policy(
1954 identity: &DualRunScenarioIdentity,
1955 verdict: &ComparisonVerdict,
1956 noise_class: SchedulerNoiseClass,
1957) -> TimePolicyClass {
1958 let has_timer_contract = [
1959 "scenario_clock_id",
1960 "logical_deadline_id",
1961 "normalization_window",
1962 ]
1963 .iter()
1964 .all(|key| identity.metadata.contains_key(*key));
1965 let has_time_mismatch = verdict.mismatches.iter().any(|mismatch| {
1966 mismatch.field.contains("timeout")
1967 || mismatch.field.contains("deadline")
1968 || mismatch.field.contains("clock")
1969 });
1970
1971 if has_time_mismatch && has_timer_contract {
1972 return TimePolicyClass::SemanticTime;
1973 }
1974 if has_time_mismatch {
1975 return TimePolicyClass::UnsupportedTimeSurface;
1976 }
1977 if noise_class != SchedulerNoiseClass::None && verdict.mismatches.is_empty() {
1978 return TimePolicyClass::SchedulerNoiseSignal;
1979 }
1980 TimePolicyClass::NotApplicable
1981}
1982
1983fn eligibility_verdict(identity: &DualRunScenarioIdentity) -> Option<&str> {
1984 identity
1985 .metadata
1986 .get("eligibility_verdict")
1987 .map(String::as_str)
1988}
1989
1990fn is_bridge_only_downgrade(identity: &DualRunScenarioIdentity) -> bool {
1991 let has_bridge_only_support_class = matches!(
1992 identity.metadata.get("support_class").map(String::as_str),
1993 Some("bridge_only")
1994 );
1995
1996 let has_supported_downgrade_reason = matches!(
1997 identity.metadata.get("reason_code").map(String::as_str),
1998 Some(
1999 "downgrade_to_server_bridge"
2000 | "downgrade_to_edge_bridge"
2001 | "downgrade_to_websocket_or_fetch"
2002 | "downgrade_to_export_bytes_for_download"
2003 | "downgrade_to_bridge_only"
2004 )
2005 );
2006
2007 has_bridge_only_support_class && has_supported_downgrade_reason
2008}
2009
2010fn unsupported_surface_reason(identity: &DualRunScenarioIdentity) -> Option<String> {
2011 if let Some(
2012 verdict @ ("blocked_missing_virtualization"
2013 | "blocked_missing_verification"
2014 | "blocked_scope_red_line"
2015 | "unsupported"
2016 | "rejected"
2017 | "unsupported_surface"),
2018 ) = eligibility_verdict(identity)
2019 {
2020 return Some(format!("eligibility_verdict={verdict}"));
2021 }
2022
2023 if let Some(class) = identity.metadata.get("support_class") {
2024 if matches!(class.as_str(), "unsupported" | "unsupported_surface") {
2025 return Some(format!("support_class={class}"));
2026 }
2027 }
2028
2029 if is_bridge_only_downgrade(identity) {
2030 return None;
2031 }
2032
2033 if let Some(reason) = identity.metadata.get("unsupported_reason") {
2034 return Some(reason.clone());
2035 }
2036
2037 None
2038}
2039
2040fn insufficient_observability_reason(
2041 identity: &DualRunScenarioIdentity,
2042 verdict: &ComparisonVerdict,
2043 live: &NormalizedObservable,
2044) -> Option<String> {
2045 if matches!(
2046 eligibility_verdict(identity),
2047 Some("blocked_missing_observability")
2048 ) {
2049 return Some("eligibility_verdict=blocked_missing_observability".to_string());
2050 }
2051
2052 if let Some(status) = identity.metadata.get("observability_status") {
2053 let lowered = status.to_ascii_lowercase();
2054 if ["blocked", "missing", "limited", "insufficient"]
2055 .iter()
2056 .any(|needle| lowered.contains(needle))
2057 {
2058 return Some(status.clone());
2059 }
2060 }
2061
2062 if verdict
2063 .mismatches
2064 .iter()
2065 .any(|mismatch| mismatch.description.contains("missing in live observable"))
2066 && live
2067 .semantics
2068 .resource_surface
2069 .tolerances
2070 .values()
2071 .any(|tolerance| *tolerance == CounterTolerance::Unsupported)
2072 {
2073 return Some(
2074 "live observable omitted a required counter while declaring unsupported tolerance"
2075 .to_string(),
2076 );
2077 }
2078
2079 None
2080}
2081
2082fn hard_contract_break_reason(
2083 live: &NormalizedObservable,
2084 live_invariant_violations: &[String],
2085) -> Option<String> {
2086 if !live_invariant_violations.is_empty() {
2087 return Some(format!(
2088 "live invariant violations: {}",
2089 live_invariant_violations.join("; ")
2090 ));
2091 }
2092 if live.semantics.obligation_balance.leaked > 0 {
2093 return Some("live run leaked obligations".to_string());
2094 }
2095 if live.semantics.obligation_balance.unresolved > 0 {
2096 return Some("live run left obligations unresolved".to_string());
2097 }
2098 if live.semantics.loser_drain.applicable
2099 && live.semantics.loser_drain.status != DrainStatus::Complete
2100 {
2101 return Some("live run did not complete loser drain".to_string());
2102 }
2103 if !live.semantics.region_close.quiescent {
2104 return Some("live root region did not close to quiescence".to_string());
2105 }
2106 if live.semantics.terminal_outcome.class == OutcomeClass::Panicked {
2107 return Some("live run panicked on an admitted surface".to_string());
2108 }
2109 if live.semantics.cancellation.acknowledged
2110 && (!live.semantics.cancellation.cleanup_completed
2111 || !live.semantics.cancellation.finalization_completed)
2112 {
2113 return Some("live cancellation acknowledged without cleanup/finalization".to_string());
2114 }
2115 None
2116}
2117
2118fn terminal_policy_outcome(
2119 provisional_class: ProvisionalDivergenceClass,
2120 rerun_decision: RerunDecision,
2121 suggested_final_class: Option<FinalDivergenceClass>,
2122 time_policy_class: TimePolicyClass,
2123 scheduler_noise_class: SchedulerNoiseClass,
2124 suppression_reason: Option<String>,
2125 explanation: impl Into<String>,
2126) -> DifferentialPolicyOutcome {
2127 DifferentialPolicyOutcome {
2128 provisional_class,
2129 rerun_decision,
2130 suggested_final_class,
2131 time_policy_class,
2132 scheduler_noise_class,
2133 suppression_reason,
2134 explanation: explanation.into(),
2135 }
2136}
2137
2138fn classify_differential_policy(
2139 identity: &DualRunScenarioIdentity,
2140 lab: &NormalizedObservable,
2141 live: &NormalizedObservable,
2142 verdict: &ComparisonVerdict,
2143 lab_invariant_violations: &[String],
2144 live_invariant_violations: &[String],
2145) -> DifferentialPolicyOutcome {
2146 let noise_class = classify_scheduler_noise(lab, live);
2147 let time_policy_class = classify_time_policy(identity, verdict, noise_class);
2148
2149 if lab.schema_version != live.schema_version {
2150 return terminal_policy_outcome(
2151 ProvisionalDivergenceClass::ArtifactSchemaViolation,
2152 RerunDecision::None,
2153 Some(FinalDivergenceClass::ArtifactSchemaViolation),
2154 time_policy_class,
2155 noise_class,
2156 Some("schema version mismatch".to_string()),
2157 "comparison artifacts do not share a schema contract, so reruns would not be honest",
2158 );
2159 }
2160
2161 if let Some(reason) = unsupported_surface_reason(identity) {
2162 return terminal_policy_outcome(
2163 ProvisionalDivergenceClass::UnsupportedSurface,
2164 RerunDecision::None,
2165 Some(FinalDivergenceClass::UnsupportedSurface),
2166 time_policy_class,
2167 noise_class,
2168 Some(reason),
2169 "scenario metadata marks this surface unsupported, so the mismatch is rejected immediately",
2170 );
2171 }
2172
2173 if let Some(reason) = insufficient_observability_reason(identity, verdict, live) {
2174 return terminal_policy_outcome(
2175 ProvisionalDivergenceClass::InsufficientObservability,
2176 RerunDecision::ConfirmationIfRicherInstrumentationEnabled { additional_runs: 1 },
2177 Some(FinalDivergenceClass::InsufficientObservability),
2178 time_policy_class,
2179 noise_class,
2180 Some(reason),
2181 "required evidence is missing or explicitly blocked, so this surface cannot be promoted honestly",
2182 );
2183 }
2184
2185 if let Some(reason) = hard_contract_break_reason(live, live_invariant_violations) {
2186 return terminal_policy_outcome(
2187 ProvisionalDivergenceClass::HardContractBreak,
2188 RerunDecision::None,
2189 Some(FinalDivergenceClass::RuntimeSemanticBug),
2190 time_policy_class,
2191 noise_class,
2192 Some(reason),
2193 "the live side already violates a hard semantic contract, so the framework should escalate immediately",
2194 );
2195 }
2196
2197 if verdict.passed
2198 && lab_invariant_violations.is_empty()
2199 && live_invariant_violations.is_empty()
2200 && noise_class != SchedulerNoiseClass::None
2201 {
2202 return terminal_policy_outcome(
2203 ProvisionalDivergenceClass::SchedulerNoiseSuspected,
2204 RerunDecision::LiveConfirmations { additional_runs: 2 },
2205 Some(FinalDivergenceClass::SchedulerNoiseSuspected),
2206 time_policy_class,
2207 noise_class,
2208 Some(
2209 "semantic observables stayed equal while only scheduler/provenance signals drifted"
2210 .to_string(),
2211 ),
2212 "the semantic verdict remains a pass, but the report should retain scheduler-noise triage metadata",
2213 );
2214 }
2215
2216 if verdict.passed && lab_invariant_violations.is_empty() && live_invariant_violations.is_empty()
2217 {
2218 return terminal_policy_outcome(
2219 ProvisionalDivergenceClass::Pass,
2220 RerunDecision::None,
2221 None,
2222 time_policy_class,
2223 noise_class,
2224 None,
2225 "semantic observables match and no invariant failures were observed",
2226 );
2227 }
2228
2229 terminal_policy_outcome(
2230 ProvisionalDivergenceClass::SemanticMismatchAdmittedSurface,
2231 RerunDecision::DeterministicLabReplayAndLiveConfirmations {
2232 additional_live_runs: 2,
2233 },
2234 None,
2235 time_policy_class,
2236 noise_class,
2237 None,
2238 "semantic mismatches survived the initial comparison on an admitted surface; schedule the canonical lab replay plus two live confirmation reruns",
2239 )
2240}
2241
2242#[must_use]
2252pub fn check_core_invariants(obs: &NormalizedObservable) -> Vec<String> {
2253 let mut violations = Vec::new();
2254
2255 if !obs.semantics.obligation_balance.balanced {
2257 violations.push(format!(
2258 "Obligation balance: leaked={}, unresolved={}",
2259 obs.semantics.obligation_balance.leaked, obs.semantics.obligation_balance.unresolved
2260 ));
2261 }
2262
2263 if !obs.semantics.region_close.quiescent {
2265 violations.push(format!(
2266 "Region not quiescent: state={:?}, live_children={}, finalizers_pending={}",
2267 obs.semantics.region_close.root_state,
2268 obs.semantics.region_close.live_children,
2269 obs.semantics.region_close.finalizers_pending
2270 ));
2271 }
2272
2273 if obs.semantics.loser_drain.applicable
2275 && obs.semantics.loser_drain.status == DrainStatus::Incomplete
2276 {
2277 violations.push(format!(
2278 "Incomplete loser drain: expected={}, drained={}",
2279 obs.semantics.loser_drain.expected_losers, obs.semantics.loser_drain.drained_losers
2280 ));
2281 }
2282
2283 if obs.semantics.cancellation.requested && !obs.semantics.cancellation.cleanup_completed {
2285 violations.push(format!(
2286 "Cancellation cleanup incomplete: phase={:?}",
2287 obs.semantics.cancellation.terminal_phase
2288 ));
2289 }
2290 if obs.semantics.cancellation.requested
2291 && obs.semantics.cancellation.cleanup_completed
2292 && !obs.semantics.cancellation.finalization_completed
2293 {
2294 violations.push(format!(
2295 "Cancellation finalization incomplete: phase={:?}",
2296 obs.semantics.cancellation.terminal_phase
2297 ));
2298 }
2299
2300 violations
2301}
2302
2303#[must_use]
2307pub fn assert_semantics(
2308 actual: &NormalizedSemantics,
2309 expected: &NormalizedSemantics,
2310) -> Vec<SemanticMismatch> {
2311 let lab = NormalizedObservable {
2313 schema_version: NORMALIZED_OBSERVABLE_SCHEMA_VERSION.to_string(),
2314 scenario_id: String::new(),
2315 surface_id: String::new(),
2316 surface_contract_version: String::new(),
2317 runtime_kind: RuntimeKind::Lab,
2318 semantics: expected.clone(),
2319 provenance: ReplayMetadata::for_lab(
2320 ScenarioFamilyId::new("", "", ""),
2321 &SeedPlan::inherit(0, ""),
2322 ),
2323 };
2324 let live = NormalizedObservable {
2325 schema_version: NORMALIZED_OBSERVABLE_SCHEMA_VERSION.to_string(),
2326 scenario_id: String::new(),
2327 surface_id: String::new(),
2328 surface_contract_version: String::new(),
2329 runtime_kind: RuntimeKind::Live,
2330 semantics: actual.clone(),
2331 provenance: ReplayMetadata::for_live(
2332 ScenarioFamilyId::new("", "", ""),
2333 &SeedPlan::inherit(0, ""),
2334 ),
2335 };
2336
2337 let verdict = compare_observables(
2338 &lab,
2339 &live,
2340 SeedLineageRecord::from_plan(&SeedPlan::inherit(0, "")),
2341 );
2342 verdict.mismatches
2343}
2344
2345#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2351#[serde(rename_all = "snake_case")]
2352pub enum LiveExecutionProfile {
2353 CurrentThread,
2356}
2357
2358impl fmt::Display for LiveExecutionProfile {
2359 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2360 match self {
2361 Self::CurrentThread => write!(f, "phase1.current_thread"),
2362 }
2363 }
2364}
2365
2366#[derive(Debug, Clone, Serialize, Deserialize)]
2368pub struct LiveRunnerConfig {
2369 pub seed: u64,
2371 pub entropy_seed: u64,
2373 pub profile: LiveExecutionProfile,
2375 pub scenario_id: String,
2377 pub surface_id: String,
2379 pub seed_lineage_id: String,
2381}
2382
2383impl LiveRunnerConfig {
2384 #[must_use]
2386 pub fn from_identity(identity: &DualRunScenarioIdentity) -> Self {
2387 let live_seed = identity.seed_plan.effective_live_seed();
2388 let entropy = identity.seed_plan.effective_entropy_seed(live_seed);
2389 Self {
2390 seed: live_seed,
2391 entropy_seed: entropy,
2392 profile: LiveExecutionProfile::CurrentThread,
2393 scenario_id: identity.scenario_id.clone(),
2394 surface_id: identity.surface_id.clone(),
2395 seed_lineage_id: identity.seed_plan.seed_lineage_id.clone(),
2396 }
2397 }
2398
2399 #[must_use]
2401 pub fn from_plan(
2402 plan: &SeedPlan,
2403 scenario_id: impl Into<String>,
2404 surface_id: impl Into<String>,
2405 ) -> Self {
2406 let live_seed = plan.effective_live_seed();
2407 let entropy = plan.effective_entropy_seed(live_seed);
2408 Self {
2409 seed: live_seed,
2410 entropy_seed: entropy,
2411 profile: LiveExecutionProfile::CurrentThread,
2412 scenario_id: scenario_id.into(),
2413 surface_id: surface_id.into(),
2414 seed_lineage_id: plan.seed_lineage_id.clone(),
2415 }
2416 }
2417}
2418
2419impl fmt::Display for LiveRunnerConfig {
2420 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2421 write!(
2422 f,
2423 "LiveRunner(scenario={}, surface={}, seed=0x{:X}, profile={})",
2424 self.scenario_id, self.surface_id, self.seed, self.profile
2425 )
2426 }
2427}
2428
2429#[derive(Debug, Clone)]
2440pub struct LiveWitnessCollector {
2441 terminal_outcome: TerminalOutcome,
2442 cancellation: CancellationRecord,
2443 loser_drain: LoserDrainRecord,
2444 region_close: RegionCloseRecord,
2445 obligation_balance: ObligationBalanceRecord,
2446 resource_surface: ResourceSurfaceRecord,
2447 manifest: CaptureManifest,
2448 nondeterminism_notes: Vec<String>,
2450}
2451
2452impl LiveWitnessCollector {
2453 #[must_use]
2458 pub fn new(surface_scope: impl Into<String>) -> Self {
2459 let surface_scope = surface_scope.into();
2460 let mut manifest = CaptureManifest::new();
2461 manifest.inferred("terminal_outcome", "run_live_adapter.default_ok");
2462 manifest.inferred("cancellation", "run_live_adapter.default_no_cancellation");
2463 manifest.unsupported("cancellation.checkpoint_observed");
2464 manifest.inferred("loser_drain", "run_live_adapter.default_not_applicable");
2465 manifest.inferred("region_close", "run_live_adapter.default_quiescent");
2466 manifest.inferred(
2467 "obligation_balance",
2468 "run_live_adapter.default_balanced_obligations",
2469 );
2470 manifest.observed(
2471 "resource_surface.contract_scope",
2472 "scenario_identity.surface_id",
2473 );
2474
2475 Self {
2476 terminal_outcome: TerminalOutcome::ok(),
2477 cancellation: CancellationRecord::none(),
2478 loser_drain: LoserDrainRecord::not_applicable(),
2479 region_close: RegionCloseRecord::quiescent(),
2480 obligation_balance: ObligationBalanceRecord::zero(),
2481 resource_surface: ResourceSurfaceRecord::empty(surface_scope),
2482 manifest,
2483 nondeterminism_notes: Vec::new(),
2484 }
2485 }
2486
2487 pub fn set_outcome(&mut self, outcome: TerminalOutcome) {
2489 self.terminal_outcome = outcome;
2490 self.manifest
2491 .observed("terminal_outcome", "witness.set_outcome");
2492 }
2493
2494 pub fn set_cancellation(&mut self, record: CancellationRecord) {
2496 if record.checkpoint_observed.is_some() {
2497 self.manifest.observed(
2498 "cancellation.checkpoint_observed",
2499 "witness.set_cancellation",
2500 );
2501 } else {
2502 self.manifest
2503 .unsupported("cancellation.checkpoint_observed");
2504 }
2505 self.cancellation = record;
2506 self.manifest
2507 .observed("cancellation", "witness.set_cancellation");
2508 }
2509
2510 pub fn set_loser_drain(&mut self, record: LoserDrainRecord) {
2512 self.loser_drain = record;
2513 self.manifest
2514 .observed("loser_drain", "witness.set_loser_drain");
2515 }
2516
2517 pub fn set_region_close(&mut self, record: RegionCloseRecord) {
2519 self.region_close = record;
2520 self.manifest
2521 .observed("region_close", "witness.set_region_close");
2522 }
2523
2524 pub fn set_obligation_balance(&mut self, record: ObligationBalanceRecord) {
2526 self.obligation_balance = record;
2527 self.manifest
2528 .observed("obligation_balance", "witness.set_obligation_balance");
2529 }
2530
2531 pub fn record_counter(&mut self, name: impl Into<String>, value: i64) {
2533 let n = name.into();
2534 let counter_manifest_key = format!("resource_surface.counters.{n}");
2535 let tolerance_manifest_key = format!("resource_surface.tolerances.{n}");
2536 self.resource_surface.counters.insert(n.clone(), value);
2537 self.resource_surface
2538 .tolerances
2539 .insert(n, CounterTolerance::Exact);
2540 self.manifest
2541 .observed(counter_manifest_key, "witness.record_counter");
2542 self.manifest
2543 .observed(tolerance_manifest_key, "witness.record_counter");
2544 }
2545
2546 pub fn record_counter_with_tolerance(
2548 &mut self,
2549 name: impl Into<String>,
2550 value: i64,
2551 tolerance: CounterTolerance,
2552 ) {
2553 let n = name.into();
2554 self.resource_surface.counters.insert(n.clone(), value);
2555 self.resource_surface
2556 .tolerances
2557 .insert(n.clone(), tolerance);
2558 self.manifest.observed(
2559 format!("resource_surface.counters.{n}"),
2560 "witness.record_counter_with_tolerance",
2561 );
2562 self.manifest.observed(
2563 format!("resource_surface.tolerances.{n}"),
2564 "witness.record_counter_with_tolerance",
2565 );
2566 }
2567
2568 pub fn note_nondeterminism(&mut self, note: impl Into<String>) {
2570 self.nondeterminism_notes.push(note.into());
2571 }
2572
2573 #[must_use]
2575 pub fn finalize(self) -> NormalizedSemantics {
2576 NormalizedSemantics {
2577 terminal_outcome: self.terminal_outcome,
2578 cancellation: self.cancellation,
2579 loser_drain: self.loser_drain,
2580 region_close: self.region_close,
2581 obligation_balance: self.obligation_balance,
2582 resource_surface: self.resource_surface,
2583 }
2584 }
2585
2586 #[must_use]
2588 pub fn capture_manifest(&self) -> &CaptureManifest {
2589 &self.manifest
2590 }
2591
2592 #[must_use]
2594 pub fn nondeterminism_notes(&self) -> &[String] {
2595 &self.nondeterminism_notes
2596 }
2597}
2598
2599#[derive(Debug, Clone, Serialize, Deserialize)]
2601pub struct LiveRunMetadata {
2602 pub config: LiveRunnerConfig,
2604 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2606 pub nondeterminism_notes: Vec<String>,
2607 pub capture_manifest: CaptureManifest,
2609 pub replay: ReplayMetadata,
2611}
2612
2613#[derive(Debug, Clone)]
2615pub struct LiveRunResult {
2616 pub semantics: NormalizedSemantics,
2618 pub metadata: LiveRunMetadata,
2620}
2621
2622#[must_use]
2648pub fn run_live_adapter(
2649 identity: &DualRunScenarioIdentity,
2650 f: impl FnOnce(&LiveRunnerConfig, &mut LiveWitnessCollector),
2651) -> LiveRunResult {
2652 let config = LiveRunnerConfig::from_identity(identity);
2653 let mut witness = LiveWitnessCollector::new(&identity.surface_id);
2654
2655 #[cfg(feature = "tracing-integration")]
2656 tracing::info!(
2657 scenario_id = %identity.scenario_id,
2658 surface_id = %identity.surface_id,
2659 seed = %format_args!("0x{:X}", config.seed),
2660 entropy_seed = %format_args!("0x{:X}", config.entropy_seed),
2661 profile = %config.profile,
2662 seed_lineage = %config.seed_lineage_id,
2663 "LIVE_RUN_START"
2664 );
2665
2666 f(&config, &mut witness);
2667
2668 let nondeterminism_notes = witness.nondeterminism_notes().to_vec();
2669 let capture_manifest = witness.capture_manifest().clone();
2670 let semantics = witness.finalize();
2671 let replay = ReplayMetadata::for_live(identity.family_id(), &identity.seed_plan)
2672 .with_nondeterminism_notes(nondeterminism_notes.clone());
2673
2674 #[cfg(feature = "tracing-integration")]
2675 tracing::info!(
2676 scenario_id = %identity.scenario_id,
2677 outcome = %semantics.terminal_outcome.class,
2678 quiescent = semantics.region_close.quiescent,
2679 obligation_balanced = semantics.obligation_balance.balanced,
2680 nondeterminism_count = nondeterminism_notes.len(),
2681 "LIVE_RUN_COMPLETE"
2682 );
2683
2684 LiveRunResult {
2685 semantics,
2686 metadata: LiveRunMetadata {
2687 config,
2688 nondeterminism_notes,
2689 capture_manifest,
2690 replay,
2691 },
2692 }
2693}
2694
2695#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2704#[serde(rename_all = "snake_case")]
2705pub enum FieldObservability {
2706 Observed,
2708 Inferred,
2710 Unsupported,
2712}
2713
2714impl fmt::Display for FieldObservability {
2715 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2716 match self {
2717 Self::Observed => write!(f, "observed"),
2718 Self::Inferred => write!(f, "inferred"),
2719 Self::Unsupported => write!(f, "unsupported"),
2720 }
2721 }
2722}
2723
2724#[derive(Debug, Clone, Serialize, Deserialize)]
2726pub struct CaptureAnnotation {
2727 pub field: String,
2729 pub observability: FieldObservability,
2731 pub source: String,
2733}
2734
2735#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2740pub struct CaptureManifest {
2741 pub annotations: Vec<CaptureAnnotation>,
2743 pub unsupported_fields: Vec<String>,
2745}
2746
2747impl CaptureManifest {
2748 fn upsert(&mut self, field: String, observability: FieldObservability, source: String) {
2749 self.unsupported_fields
2750 .retain(|existing| existing != &field);
2751 if observability == FieldObservability::Unsupported {
2752 self.unsupported_fields.push(field.clone());
2753 self.unsupported_fields.sort_unstable();
2754 self.unsupported_fields.dedup();
2755 }
2756
2757 if let Some(annotation) = self.annotations.iter_mut().find(|a| a.field == field) {
2758 annotation.observability = observability;
2759 annotation.source = source;
2760 } else {
2761 self.annotations.push(CaptureAnnotation {
2762 field,
2763 observability,
2764 source,
2765 });
2766 }
2767 self.annotations.sort_by(|left, right| {
2768 left.field
2769 .cmp(&right.field)
2770 .then(left.source.cmp(&right.source))
2771 });
2772 }
2773
2774 fn annotation_for_candidate(&self, field: &str) -> Option<&CaptureAnnotation> {
2775 self.annotations
2776 .iter()
2777 .find(|annotation| annotation.field == field)
2778 }
2779
2780 #[must_use]
2782 pub fn new() -> Self {
2783 Self::default()
2784 }
2785
2786 pub fn observed(&mut self, field: impl Into<String>, source: impl Into<String>) {
2788 self.upsert(field.into(), FieldObservability::Observed, source.into());
2789 }
2790
2791 pub fn inferred(&mut self, field: impl Into<String>, source: impl Into<String>) {
2793 self.upsert(field.into(), FieldObservability::Inferred, source.into());
2794 }
2795
2796 pub fn unsupported(&mut self, field: impl Into<String>) {
2798 self.upsert(
2799 field.into(),
2800 FieldObservability::Unsupported,
2801 "default".to_string(),
2802 );
2803 }
2804
2805 #[must_use]
2807 pub fn total_fields(&self) -> usize {
2808 self.annotations.len()
2809 }
2810
2811 #[must_use]
2813 pub fn unsupported_count(&self) -> usize {
2814 self.unsupported_fields.len()
2815 }
2816
2817 #[must_use]
2819 pub fn fully_observed(&self) -> bool {
2820 !self.annotations.is_empty()
2821 && self
2822 .annotations
2823 .iter()
2824 .all(|a| a.observability == FieldObservability::Observed)
2825 }
2826
2827 #[must_use]
2830 pub fn annotation_for_field(&self, field: &str) -> Option<&CaptureAnnotation> {
2831 if let Some(annotation) = self.annotation_for_candidate(field) {
2832 return Some(annotation);
2833 }
2834
2835 let normalized = field.strip_prefix("semantics.").unwrap_or(field);
2836 if let Some(annotation) = self.annotation_for_candidate(normalized) {
2837 return Some(annotation);
2838 }
2839
2840 let mut candidate = normalized;
2841 while let Some((parent, _)) = candidate.rsplit_once('.') {
2842 if let Some(annotation) = self.annotation_for_candidate(parent) {
2843 return Some(annotation);
2844 }
2845 candidate = parent;
2846 }
2847
2848 None
2849 }
2850
2851 #[must_use]
2853 pub fn describe_field_capture(&self, field: &str) -> Option<String> {
2854 self.annotation_for_field(field)
2855 .map(|annotation| format!("{} via {}", annotation.observability, annotation.source))
2856 }
2857}
2858
2859#[must_use]
2865pub fn capture_terminal_outcome<T, E: fmt::Display>(
2866 outcome: &crate::types::outcome::Outcome<T, E>,
2867) -> TerminalOutcome {
2868 match outcome {
2869 crate::types::outcome::Outcome::Ok(_) => TerminalOutcome::ok(),
2870 crate::types::outcome::Outcome::Err(e) => TerminalOutcome::err(format!("{e}")),
2871 crate::types::outcome::Outcome::Cancelled(reason) => {
2872 TerminalOutcome::cancelled(format!("{reason}"))
2873 }
2874 crate::types::outcome::Outcome::Panicked(_) => TerminalOutcome {
2875 class: OutcomeClass::Panicked,
2876 severity: OutcomeClass::Panicked,
2877 surface_result: None,
2878 error_class: None,
2879 cancel_reason_class: None,
2880 panic_class: Some("caught_panic".to_string()),
2881 },
2882 }
2883}
2884
2885#[must_use]
2889pub fn capture_terminal_from_result<T, E: fmt::Display>(result: &Result<T, E>) -> TerminalOutcome {
2890 match result {
2891 Ok(_) => TerminalOutcome::ok(),
2892 Err(e) => TerminalOutcome::err(format!("{e}")),
2893 }
2894}
2895
2896#[must_use]
2901pub fn capture_obligation_balance(
2902 reserved: u32,
2903 committed: u32,
2904 aborted: u32,
2905) -> ObligationBalanceRecord {
2906 let leaked = reserved.saturating_sub(committed.saturating_add(aborted));
2907 ObligationBalanceRecord {
2908 reserved,
2909 committed,
2910 aborted,
2911 leaked,
2912 unresolved: 0,
2913 balanced: leaked == 0,
2914 }
2915 .recompute()
2916}
2917
2918#[must_use]
2922pub fn capture_region_close(
2923 all_children_joined: bool,
2924 all_finalizers_done: bool,
2925) -> RegionCloseRecord {
2926 let quiescent = all_children_joined && all_finalizers_done;
2927 RegionCloseRecord {
2928 root_state: if quiescent {
2932 RegionState::Closed
2933 } else if all_children_joined {
2934 RegionState::Finalizing
2935 } else {
2936 RegionState::Draining
2937 },
2938 quiescent,
2939 live_children: u32::from(!all_children_joined),
2940 finalizers_pending: u32::from(!all_finalizers_done),
2941 close_completed: quiescent,
2942 }
2943}
2944
2945#[must_use]
2950pub fn capture_loser_drain(loser_joined: &[bool]) -> LoserDrainRecord {
2951 if loser_joined.is_empty() {
2952 return LoserDrainRecord::not_applicable();
2953 }
2954 let expected = loser_joined.len() as u32;
2955 let drained = loser_joined.iter().filter(|&&x| x).count() as u32;
2956 LoserDrainRecord {
2957 applicable: true,
2958 expected_losers: expected,
2959 drained_losers: drained,
2960 status: if drained == expected {
2961 DrainStatus::Complete
2962 } else {
2963 DrainStatus::Incomplete
2964 },
2965 evidence: Some("task_handle.join".to_string()),
2966 }
2967}
2968
2969#[must_use]
2971#[allow(clippy::fn_params_excessive_bools)]
2972pub fn capture_cancellation(
2973 requested: bool,
2974 acknowledged: bool,
2975 cleanup_completed: bool,
2976 finalization_completed: bool,
2977 checkpoint_observed: Option<bool>,
2978) -> CancellationRecord {
2979 let terminal_phase = if !requested {
2980 CancelTerminalPhase::NotCancelled
2981 } else if finalization_completed {
2982 CancelTerminalPhase::Completed
2983 } else if cleanup_completed {
2984 CancelTerminalPhase::Finalizing
2985 } else if acknowledged {
2986 CancelTerminalPhase::Cancelling
2987 } else {
2988 CancelTerminalPhase::CancelRequested
2989 };
2990
2991 CancellationRecord {
2992 requested,
2993 acknowledged,
2994 cleanup_completed,
2995 finalization_completed,
2996 terminal_phase,
2997 checkpoint_observed,
2998 }
2999}
3000
3001#[must_use]
3016#[allow(clippy::too_many_lines)]
3017pub fn normalize_lab_report(
3018 report: &crate::lab::runtime::LabRunReport,
3019 surface_scope: &str,
3020) -> (NormalizedSemantics, CaptureManifest) {
3021 let mut manifest = CaptureManifest::new();
3022
3023 let terminal_outcome = if !report.invariant_violations.is_empty() {
3025 manifest.observed("terminal_outcome", "invariant_violations");
3026 TerminalOutcome::err("invariant_violation")
3027 } else if !report.oracle_report.all_passed() {
3028 manifest.observed("terminal_outcome", "oracle_report.failures");
3029 TerminalOutcome::err("oracle_failure")
3030 } else {
3031 manifest.observed("terminal_outcome", "oracle_report.all_passed");
3032 TerminalOutcome::ok()
3033 };
3034
3035 manifest.observed("region_close.quiescent", "LabRunReport.quiescent");
3037 let region_close = RegionCloseRecord {
3038 root_state: if report.quiescent {
3039 RegionState::Closed
3040 } else {
3041 RegionState::Closing
3042 },
3043 quiescent: report.quiescent,
3044 live_children: 0,
3045 finalizers_pending: 0,
3046 close_completed: report.quiescent,
3047 };
3048
3049 let has_leak = report
3051 .invariant_violations
3052 .iter()
3053 .any(|v| v.contains("obligation") || v.contains("leak"));
3054 let obligation_oracle_failed = report
3055 .oracle_report
3056 .entry("obligation_leak")
3057 .is_some_and(|e| !e.passed);
3058 manifest.observed("obligation_balance", "oracle.obligation_leak + invariants");
3059 let obligation_balance = if has_leak || obligation_oracle_failed {
3060 ObligationBalanceRecord {
3061 reserved: 0,
3062 committed: 0,
3063 aborted: 0,
3064 leaked: 1,
3065 unresolved: 0,
3066 balanced: false,
3067 }
3068 } else {
3069 ObligationBalanceRecord::zero()
3070 };
3071
3072 let loser_drain_entry = report.oracle_report.entry("loser_drain");
3074 let loser_drain = if let Some(entry) = loser_drain_entry {
3075 manifest.observed("loser_drain", "oracle.loser_drain");
3076 if entry.passed {
3077 LoserDrainRecord {
3079 applicable: true,
3080 expected_losers: 0,
3081 drained_losers: 0,
3082 status: DrainStatus::Complete,
3083 evidence: Some("oracle.loser_drain.passed".to_string()),
3084 }
3085 } else {
3086 LoserDrainRecord {
3087 applicable: true,
3088 expected_losers: 0,
3089 drained_losers: 0,
3090 status: DrainStatus::Incomplete,
3091 evidence: Some("oracle.loser_drain.failed".to_string()),
3092 }
3093 }
3094 } else {
3095 manifest.inferred("loser_drain", "no_oracle_entry");
3096 LoserDrainRecord::not_applicable()
3097 };
3098
3099 let cancel_entry = report.oracle_report.entry("cancellation_protocol");
3101 let cancellation = if let Some(entry) = cancel_entry {
3102 manifest.observed("cancellation", "oracle.cancellation_protocol");
3103 if entry.passed {
3104 CancellationRecord::completed()
3105 } else {
3106 CancellationRecord {
3107 requested: true,
3108 acknowledged: false,
3109 cleanup_completed: false,
3110 finalization_completed: false,
3111 terminal_phase: CancelTerminalPhase::CancelRequested,
3112 checkpoint_observed: None,
3113 }
3114 }
3115 } else {
3116 manifest.inferred("cancellation", "no_oracle_entry");
3117 CancellationRecord::none()
3118 };
3119
3120 let semantics = NormalizedSemantics {
3121 terminal_outcome,
3122 cancellation,
3123 loser_drain,
3124 region_close,
3125 obligation_balance,
3126 resource_surface: ResourceSurfaceRecord::empty(surface_scope),
3127 };
3128
3129 (semantics, manifest)
3130}
3131
3132#[must_use]
3136pub fn normalize_lab_observable(
3137 identity: &DualRunScenarioIdentity,
3138 report: &crate::lab::runtime::LabRunReport,
3139) -> NormalizedObservable {
3140 let (semantics, _manifest) = normalize_lab_report(report, &identity.surface_id);
3141 let mut prov = ReplayMetadata::for_lab(identity.family_id(), &identity.seed_plan);
3142 prov = prov.with_lab_report(
3143 report.trace_fingerprint,
3144 report.trace_certificate.event_hash,
3145 report.trace_certificate.event_count,
3146 report.trace_certificate.schedule_hash,
3147 report.steps_total,
3148 );
3149 NormalizedObservable::new(identity, RuntimeKind::Lab, semantics, prov)
3150}
3151
3152#[must_use]
3154pub fn normalize_live_observable(
3155 identity: &DualRunScenarioIdentity,
3156 live_result: &LiveRunResult,
3157) -> NormalizedObservable {
3158 let provenance = live_result
3159 .metadata
3160 .replay
3161 .clone()
3162 .with_nondeterminism_notes(live_result.metadata.nondeterminism_notes.clone());
3163 NormalizedObservable::new(
3164 identity,
3165 RuntimeKind::Live,
3166 live_result.semantics.clone(),
3167 provenance,
3168 )
3169}
3170
3171#[derive(Debug, Clone, Serialize, Deserialize)]
3177pub struct PromotedFuzzScenario {
3178 pub identity: DualRunScenarioIdentity,
3180 pub original_seed: u64,
3182 pub replay_seed: u64,
3184 pub violation_categories: Vec<String>,
3186 pub trace_fingerprint: u64,
3188 pub certificate_hash: u64,
3190 pub description: String,
3192 pub campaign_base_seed: Option<u64>,
3194 pub campaign_iteration: Option<usize>,
3196 #[serde(skip_serializing_if = "Option::is_none")]
3198 pub source_artifact_path: Option<String>,
3199}
3200
3201impl PromotedFuzzScenario {
3202 #[must_use]
3204 pub fn repro_command(&self) -> String {
3205 format!(
3206 "rch exec -- env ASUPERSYNC_SEED=0x{:X} cargo test {} -- --nocapture",
3207 self.replay_seed, self.identity.scenario_id
3208 )
3209 }
3210
3211 #[must_use]
3213 pub fn with_source_artifact_path(mut self, path: impl Into<String>) -> Self {
3214 self.source_artifact_path = Some(path.into());
3215 self
3216 }
3217
3218 #[must_use]
3220 pub fn lab_replay_metadata(&self) -> ReplayMetadata {
3221 let mut metadata = self
3222 .identity
3223 .lab_replay_metadata()
3224 .with_repro_command(self.repro_command());
3225 metadata.trace_fingerprint = Some(self.trace_fingerprint);
3226 if let Some(path) = &self.source_artifact_path {
3227 metadata = metadata.with_artifact_path(path.clone());
3228 }
3229 metadata
3230 }
3231}
3232
3233impl fmt::Display for PromotedFuzzScenario {
3234 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3235 write!(
3236 f,
3237 "PromotedFuzz({}, seed=0x{:X}, violations=[{}])",
3238 self.identity.scenario_id,
3239 self.replay_seed,
3240 self.violation_categories.join(", ")
3241 )
3242 }
3243}
3244
3245fn promoted_violation_categories(
3246 violations: &[crate::lab::runtime::InvariantViolation],
3247) -> Vec<String> {
3248 use crate::lab::runtime::InvariantViolation;
3249
3250 let mut categories: Vec<String> = violations
3251 .iter()
3252 .map(|violation| match violation {
3253 InvariantViolation::ObligationLeak { .. } => "obligation_leak".to_string(),
3254 InvariantViolation::TaskLeak { .. } => "task_leak".to_string(),
3255 InvariantViolation::ActorLeak { .. } => "actor_leak".to_string(),
3256 InvariantViolation::QuiescenceViolation => "quiescence_violation".to_string(),
3257 InvariantViolation::Futurelock { .. } => "futurelock".to_string(),
3258 InvariantViolation::CancellationProtocol { .. } => "cancellation_protocol".to_string(),
3259 })
3260 .collect();
3261 categories.sort_unstable();
3262 categories.dedup();
3263 categories
3264}
3265
3266#[derive(Debug, Clone, Serialize, Deserialize)]
3268pub struct PromotedExplorationScenario {
3269 pub identity: DualRunScenarioIdentity,
3271 pub replay_seed: u64,
3273 pub trace_fingerprint: u64,
3275 pub representative_schedule_hash: u64,
3277 pub original_seeds: Vec<u64>,
3279 pub violation_seeds: Vec<u64>,
3281 pub violation_summaries: Vec<String>,
3283 pub supporting_schedule_hashes: Vec<u64>,
3285 pub class_run_count: usize,
3287 pub source_total_runs: usize,
3289 pub source_unique_classes: usize,
3291 #[serde(skip_serializing_if = "Option::is_none")]
3293 pub source_artifact_path: Option<String>,
3294 pub description: String,
3296}
3297
3298impl PromotedExplorationScenario {
3299 #[must_use]
3301 pub fn repro_command(&self) -> String {
3302 format!(
3303 "rch exec -- env ASUPERSYNC_SEED=0x{:X} cargo test {} -- --nocapture",
3304 self.replay_seed, self.identity.scenario_id
3305 )
3306 }
3307
3308 #[must_use]
3310 pub fn with_source_artifact_path(mut self, path: impl Into<String>) -> Self {
3311 self.source_artifact_path = Some(path.into());
3312 self
3313 }
3314
3315 #[must_use]
3317 pub fn lab_replay_metadata(&self) -> ReplayMetadata {
3318 let mut metadata = self
3319 .identity
3320 .lab_replay_metadata()
3321 .with_repro_command(self.repro_command());
3322 metadata.trace_fingerprint = Some(self.trace_fingerprint);
3323 metadata.schedule_hash = Some(self.representative_schedule_hash);
3324 if let Some(path) = &self.source_artifact_path {
3325 metadata = metadata.with_artifact_path(path.clone());
3326 }
3327 metadata
3328 }
3329}
3330
3331impl fmt::Display for PromotedExplorationScenario {
3332 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3333 write!(
3334 f,
3335 "PromotedExploration({}, fingerprint=0x{:X}, seed=0x{:X}, runs={})",
3336 self.identity.scenario_id,
3337 self.trace_fingerprint,
3338 self.replay_seed,
3339 self.class_run_count
3340 )
3341 }
3342}
3343
3344#[must_use]
3346pub fn promote_fuzz_finding(
3347 finding: &crate::lab::fuzz::FuzzFinding,
3348 surface_id: &str,
3349 contract_version: &str,
3350) -> PromotedFuzzScenario {
3351 let replay_seed = finding.minimized_seed.unwrap_or(finding.seed);
3352 let violation_cats = promoted_violation_categories(&finding.violations);
3353 let primary_violation = violation_cats.first().map_or("unknown", String::as_str);
3354
3355 let scenario_id = format!(
3356 "fuzz.{surface_id}.{primary_violation}.seed_{:x}",
3357 replay_seed & 0xFFFF_FFFF
3358 );
3359 let description = format!(
3360 "Fuzz-discovered {primary_violation} adversarial case: {} violation(s) at seed 0x{:X}",
3361 finding.violations.len(),
3362 finding.seed
3363 );
3364
3365 let identity = DualRunScenarioIdentity::phase1(
3366 &scenario_id,
3367 surface_id,
3368 contract_version,
3369 &description,
3370 replay_seed,
3371 )
3372 .with_seed_plan(
3373 SeedPlan::inherit(replay_seed, scenario_id.clone()).with_entropy_seed(finding.entropy_seed),
3374 )
3375 .with_metadata("promoted_from", "fuzz_finding")
3376 .with_metadata("original_seed", format!("0x{:X}", finding.seed))
3377 .with_metadata("entropy_seed", format!("0x{:X}", finding.entropy_seed))
3378 .with_metadata(
3379 "trace_fingerprint",
3380 format!("0x{:X}", finding.trace_fingerprint),
3381 )
3382 .with_metadata(
3383 "certificate_hash",
3384 format!("0x{:X}", finding.certificate_hash),
3385 )
3386 .with_metadata("violation_categories", violation_cats.join(","));
3387
3388 PromotedFuzzScenario {
3389 identity,
3390 original_seed: finding.seed,
3391 replay_seed,
3392 violation_categories: violation_cats,
3393 trace_fingerprint: finding.trace_fingerprint,
3394 certificate_hash: finding.certificate_hash,
3395 description,
3396 campaign_base_seed: None,
3397 campaign_iteration: None,
3398 source_artifact_path: None,
3399 }
3400}
3401
3402#[must_use]
3404pub fn promote_regression_case(
3405 case: &crate::lab::fuzz::FuzzRegressionCase,
3406 surface_id: &str,
3407 contract_version: &str,
3408) -> PromotedFuzzScenario {
3409 let primary_violation = case
3410 .violation_categories
3411 .first()
3412 .map_or("unknown", String::as_str);
3413 let scenario_id = format!(
3414 "regression.{surface_id}.{primary_violation}.seed_{:x}",
3415 case.replay_seed & 0xFFFF_FFFF
3416 );
3417 let description = format!(
3418 "Regression case ({primary_violation}): {} violation(s), replay seed 0x{:X}",
3419 case.violation_categories.len(),
3420 case.replay_seed
3421 );
3422
3423 let identity = DualRunScenarioIdentity::phase1(
3424 &scenario_id,
3425 surface_id,
3426 contract_version,
3427 &description,
3428 case.replay_seed,
3429 )
3430 .with_seed_plan(
3431 SeedPlan::inherit(case.replay_seed, scenario_id.clone())
3432 .with_entropy_seed(case.entropy_seed),
3433 )
3434 .with_metadata("promoted_from", "regression_case")
3435 .with_metadata("original_seed", format!("0x{:X}", case.seed))
3436 .with_metadata("entropy_seed", format!("0x{:X}", case.entropy_seed))
3437 .with_metadata(
3438 "trace_fingerprint",
3439 format!("0x{:X}", case.trace_fingerprint),
3440 )
3441 .with_metadata("certificate_hash", format!("0x{:X}", case.certificate_hash))
3442 .with_metadata("violation_categories", case.violation_categories.join(","));
3443
3444 PromotedFuzzScenario {
3445 identity,
3446 original_seed: case.seed,
3447 replay_seed: case.replay_seed,
3448 violation_categories: case.violation_categories.clone(),
3449 trace_fingerprint: case.trace_fingerprint,
3450 certificate_hash: case.certificate_hash,
3451 description,
3452 campaign_base_seed: None,
3453 campaign_iteration: None,
3454 source_artifact_path: None,
3455 }
3456}
3457
3458#[must_use]
3460pub fn promote_regression_corpus(
3461 corpus: &crate::lab::fuzz::FuzzRegressionCorpus,
3462 surface_id: &str,
3463 contract_version: &str,
3464) -> Vec<PromotedFuzzScenario> {
3465 corpus
3466 .cases
3467 .iter()
3468 .enumerate()
3469 .map(|(i, case)| {
3470 let mut promoted = promote_regression_case(case, surface_id, contract_version);
3471 promoted.campaign_base_seed = Some(corpus.base_seed);
3472 promoted.campaign_iteration = Some(i);
3473 promoted.identity.metadata.insert(
3474 "campaign_base_seed".to_string(),
3475 format!("0x{:X}", corpus.base_seed),
3476 );
3477 promoted.identity.metadata.insert(
3478 "campaign_entropy_seed".to_string(),
3479 format!("0x{:X}", corpus.entropy_seed),
3480 );
3481 promoted
3482 .identity
3483 .metadata
3484 .insert("campaign_iteration".to_string(), i.to_string());
3485 promoted
3486 })
3487 .collect()
3488}
3489
3490#[must_use]
3496pub fn promote_exploration_report(
3497 report: &crate::lab::explorer::ExplorationReport,
3498 surface_id: &str,
3499 contract_version: &str,
3500) -> Vec<PromotedExplorationScenario> {
3501 #[derive(Default)]
3502 struct ClassAggregate {
3503 seeds: Vec<u64>,
3504 schedule_hashes: Vec<u64>,
3505 run_count: usize,
3506 representative_schedule_hash: Option<u64>,
3507 violation_seeds: Vec<u64>,
3508 violation_summaries: Vec<String>,
3509 }
3510
3511 let mut by_fingerprint: BTreeMap<u64, ClassAggregate> = BTreeMap::new();
3512
3513 for run in &report.runs {
3514 let entry = by_fingerprint.entry(run.fingerprint).or_default();
3515 entry.seeds.push(run.seed);
3516 entry.schedule_hashes.push(run.certificate_hash);
3517 entry.run_count += 1;
3518 if entry.representative_schedule_hash.is_none() {
3519 entry.representative_schedule_hash = Some(run.certificate_hash);
3520 }
3521 }
3522
3523 for violation in &report.violations {
3524 let entry = by_fingerprint.entry(violation.fingerprint).or_default();
3525 entry.violation_seeds.push(violation.seed);
3526 entry
3527 .violation_summaries
3528 .extend(violation.violations.iter().map(ToString::to_string));
3529 }
3530
3531 by_fingerprint
3532 .into_iter()
3533 .map(|(trace_fingerprint, mut aggregate)| {
3534 aggregate.seeds.sort_unstable();
3535 aggregate.seeds.dedup();
3536 aggregate.schedule_hashes.sort_unstable();
3537 aggregate.schedule_hashes.dedup();
3538 aggregate.violation_seeds.sort_unstable();
3539 aggregate.violation_seeds.dedup();
3540 aggregate.violation_summaries.sort();
3541 aggregate.violation_summaries.dedup();
3542
3543 let (replay_seed, representative_reason) =
3544 if let Some(seed) = aggregate.violation_seeds.first().copied() {
3545 (seed, "lowest_violation_seed")
3546 } else {
3547 (
3548 *aggregate
3549 .seeds
3550 .first()
3551 .expect("exploration class must contain at least one run"),
3552 "lowest_seed",
3553 )
3554 };
3555
3556 let representative_schedule_hash = report
3557 .runs
3558 .iter()
3559 .find(|run| run.fingerprint == trace_fingerprint && run.seed == replay_seed)
3560 .map(|run| run.certificate_hash)
3561 .or(aggregate.representative_schedule_hash)
3562 .expect("exploration class must have a representative schedule hash");
3563
3564 let scenario_id = format!(
3565 "schedule.{surface_id}.fp_{trace_fingerprint:016x}.seed_{:08x}",
3566 replay_seed & 0xFFFF_FFFF
3567 );
3568 let description = format!(
3569 "Promoted schedule exploration class 0x{trace_fingerprint:X}: {} run(s), representative seed 0x{replay_seed:X}",
3570 aggregate.run_count
3571 );
3572
3573 let identity = DualRunScenarioIdentity::phase1(
3574 &scenario_id,
3575 surface_id,
3576 contract_version,
3577 &description,
3578 replay_seed,
3579 )
3580 .with_metadata("promoted_from", "exploration_report")
3581 .with_metadata("trace_fingerprint", format!("0x{trace_fingerprint:X}"))
3582 .with_metadata("class_run_count", aggregate.run_count.to_string())
3583 .with_metadata("source_total_runs", report.total_runs.to_string())
3584 .with_metadata("source_unique_classes", report.unique_classes.to_string())
3585 .with_metadata("representative_reason", representative_reason);
3586
3587 PromotedExplorationScenario {
3588 identity,
3589 replay_seed,
3590 trace_fingerprint,
3591 representative_schedule_hash,
3592 original_seeds: aggregate.seeds,
3593 violation_seeds: aggregate.violation_seeds,
3594 violation_summaries: aggregate.violation_summaries,
3595 supporting_schedule_hashes: aggregate.schedule_hashes,
3596 class_run_count: aggregate.run_count,
3597 source_total_runs: report.total_runs,
3598 source_unique_classes: report.unique_classes,
3599 source_artifact_path: None,
3600 description,
3601 }
3602 })
3603 .collect()
3604}
3605
3606#[derive(Debug, Clone)]
3612pub struct DualRunResult {
3613 pub lab: NormalizedObservable,
3615 pub live: NormalizedObservable,
3617 pub verdict: ComparisonVerdict,
3619 pub lab_invariant_violations: Vec<String>,
3621 pub live_invariant_violations: Vec<String>,
3623 pub seed_lineage: SeedLineageRecord,
3625 pub policy: DifferentialPolicyOutcome,
3627}
3628
3629impl DualRunResult {
3630 #[must_use]
3633 pub fn passed(&self) -> bool {
3634 self.verdict.passed
3635 && self.lab_invariant_violations.is_empty()
3636 && self.live_invariant_violations.is_empty()
3637 }
3638
3639 #[must_use]
3641 pub fn summary(&self) -> String {
3642 let mut parts = vec![self.verdict.summary()];
3643 if !self.lab_invariant_violations.is_empty() {
3644 parts.push(format!(
3645 "Lab invariant violations: {}",
3646 self.lab_invariant_violations.join("; ")
3647 ));
3648 }
3649 if !self.live_invariant_violations.is_empty() {
3650 parts.push(format!(
3651 "Live invariant violations: {}",
3652 self.live_invariant_violations.join("; ")
3653 ));
3654 }
3655 parts.push(format!("Policy: {}", self.policy.summary()));
3656 parts.join("\n")
3657 }
3658}
3659
3660impl fmt::Display for DualRunResult {
3661 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3662 write!(f, "{}", self.summary())
3663 }
3664}
3665
3666pub struct DualRunHarness {
3692 identity: DualRunScenarioIdentity,
3693 lab_fn: Option<Box<dyn FnOnce(LabConfig) -> NormalizedSemantics>>,
3694 live_fn: Option<Box<dyn FnOnce(u64, u64) -> LiveExecutionCapture>>,
3695}
3696
3697#[derive(Debug, Clone)]
3698struct LiveExecutionCapture {
3699 semantics: NormalizedSemantics,
3700 replay: Option<ReplayMetadata>,
3701}
3702
3703impl From<NormalizedSemantics> for LiveExecutionCapture {
3704 fn from(semantics: NormalizedSemantics) -> Self {
3705 Self {
3706 semantics,
3707 replay: None,
3708 }
3709 }
3710}
3711
3712impl From<LiveRunResult> for LiveExecutionCapture {
3713 fn from(result: LiveRunResult) -> Self {
3714 Self {
3715 semantics: result.semantics,
3716 replay: Some(
3717 result
3718 .metadata
3719 .replay
3720 .with_nondeterminism_notes(result.metadata.nondeterminism_notes),
3721 ),
3722 }
3723 }
3724}
3725
3726impl DualRunHarness {
3727 #[must_use]
3729 pub fn phase1(
3730 scenario_id: impl Into<String>,
3731 surface_id: impl Into<String>,
3732 contract_version: impl Into<String>,
3733 description: impl Into<String>,
3734 canonical_seed: u64,
3735 ) -> Self {
3736 Self {
3737 identity: DualRunScenarioIdentity::phase1(
3738 scenario_id,
3739 surface_id,
3740 contract_version,
3741 description,
3742 canonical_seed,
3743 ),
3744 lab_fn: None,
3745 live_fn: None,
3746 }
3747 }
3748
3749 #[must_use]
3751 pub fn from_identity(identity: DualRunScenarioIdentity) -> Self {
3752 Self {
3753 identity,
3754 lab_fn: None,
3755 live_fn: None,
3756 }
3757 }
3758
3759 #[must_use]
3764 pub fn lab(mut self, f: impl FnOnce(LabConfig) -> NormalizedSemantics + 'static) -> Self {
3765 self.lab_fn = Some(Box::new(f));
3766 self
3767 }
3768
3769 #[must_use]
3774 pub fn live(mut self, f: impl FnOnce(u64, u64) -> NormalizedSemantics + 'static) -> Self {
3775 self.live_fn = Some(Box::new(move |seed, entropy| f(seed, entropy).into()));
3776 self
3777 }
3778
3779 #[must_use]
3784 pub fn live_result(mut self, f: impl FnOnce(u64, u64) -> LiveRunResult + 'static) -> Self {
3785 self.live_fn = Some(Box::new(move |seed, entropy| f(seed, entropy).into()));
3786 self
3787 }
3788
3789 #[must_use]
3791 pub fn with_seed_plan(mut self, plan: SeedPlan) -> Self {
3792 self.identity.seed_plan = plan;
3793 self
3794 }
3795
3796 #[must_use]
3802 pub fn run(self) -> DualRunResult {
3803 let lab_fn = self.lab_fn.expect("DualRunHarness: lab function not set");
3804 let live_fn = self.live_fn.expect("DualRunHarness: live function not set");
3805
3806 let plan = &self.identity.seed_plan;
3807 let family = self.identity.family_id();
3808
3809 let lab_config = plan.to_lab_config();
3811 let lab_semantics = lab_fn(lab_config);
3812 let lab_prov = ReplayMetadata::for_lab(family.clone(), plan);
3813 let lab_obs =
3814 NormalizedObservable::new(&self.identity, RuntimeKind::Lab, lab_semantics, lab_prov);
3815
3816 let live_seed = plan.effective_live_seed();
3818 let live_entropy = plan.effective_entropy_seed(live_seed);
3819 let live_capture = live_fn(live_seed, live_entropy);
3820 let live_semantics = live_capture.semantics;
3821 let live_prov = live_capture
3822 .replay
3823 .unwrap_or_else(|| ReplayMetadata::for_live(family, plan));
3824 let live_obs =
3825 NormalizedObservable::new(&self.identity, RuntimeKind::Live, live_semantics, live_prov);
3826
3827 let lab_violations = check_core_invariants(&lab_obs);
3829 let live_violations = check_core_invariants(&live_obs);
3830
3831 let lineage = SeedLineageRecord::from_plan(plan);
3833 let verdict = compare_observables(&lab_obs, &live_obs, lineage.clone());
3834 let policy = classify_differential_policy(
3835 &self.identity,
3836 &lab_obs,
3837 &live_obs,
3838 &verdict,
3839 &lab_violations,
3840 &live_violations,
3841 );
3842
3843 #[cfg(feature = "tracing-integration")]
3845 tracing::info!(
3846 scenario_id = %self.identity.scenario_id,
3847 surface_id = %self.identity.surface_id,
3848 seed = %format_args!("0x{:X}", plan.canonical_seed),
3849 passed = verdict.passed,
3850 lab_violations = lab_violations.len(),
3851 live_violations = live_violations.len(),
3852 mismatches = verdict.mismatches.len(),
3853 provisional_class = %policy.provisional_class,
3854 rerun_decision = %policy.rerun_decision,
3855 time_policy_class = %policy.time_policy_class,
3856 scheduler_noise_class = %policy.scheduler_noise_class,
3857 suppression_reason = ?policy.suppression_reason,
3858 "DUAL_RUN_RESULT"
3859 );
3860
3861 DualRunResult {
3862 lab: lab_obs,
3863 live: live_obs,
3864 verdict,
3865 lab_invariant_violations: lab_violations,
3866 live_invariant_violations: live_violations,
3867 seed_lineage: lineage,
3868 policy,
3869 }
3870 }
3871}
3872
3873pub fn assert_dual_run_passes(result: &DualRunResult) {
3877 assert!(
3878 result.passed(),
3879 "Dual-run test failed for scenario '{}' on surface '{}':\n{}",
3880 result.verdict.scenario_id,
3881 result.verdict.surface_id,
3882 result.summary()
3883 );
3884}
3885
3886#[cfg(test)]
3891mod tests {
3892 use super::*;
3893
3894 fn init_test(name: &str) {
3895 crate::test_utils::init_test_logging();
3896 crate::test_phase!(name);
3897 }
3898
3899 #[test]
3902 fn seed_mode_serde_roundtrip() {
3903 init_test("seed_mode_serde_roundtrip");
3904 let json = serde_json::to_string(&SeedMode::Inherit).unwrap();
3905 assert_eq!(json, "\"inherit\"");
3906 let parsed: SeedMode = serde_json::from_str(&json).unwrap();
3907 assert_eq!(parsed, SeedMode::Inherit);
3908
3909 let json = serde_json::to_string(&SeedMode::Override).unwrap();
3910 assert_eq!(json, "\"override\"");
3911 let parsed: SeedMode = serde_json::from_str(&json).unwrap();
3912 assert_eq!(parsed, SeedMode::Override);
3913 crate::test_complete!("seed_mode_serde_roundtrip");
3914 }
3915
3916 #[test]
3919 fn replay_policy_serde_roundtrip() {
3920 init_test("replay_policy_serde_roundtrip");
3921 for policy in [
3922 ReplayPolicy::SingleSeed,
3923 ReplayPolicy::SeedSweep,
3924 ReplayPolicy::ReplayBundle,
3925 ] {
3926 let json = serde_json::to_string(&policy).unwrap();
3927 let parsed: ReplayPolicy = serde_json::from_str(&json).unwrap();
3928 assert_eq!(parsed, policy);
3929 }
3930 crate::test_complete!("replay_policy_serde_roundtrip");
3931 }
3932
3933 #[test]
3936 fn seed_plan_inherit_uses_canonical() {
3937 init_test("seed_plan_inherit_uses_canonical");
3938 let plan = SeedPlan::inherit(0xBEEF, "test-scenario");
3939 assert_eq!(plan.effective_lab_seed(), 0xBEEF);
3940 assert_eq!(plan.effective_live_seed(), 0xBEEF);
3941 assert_eq!(plan.lab_seed_mode, SeedMode::Inherit);
3942 assert_eq!(plan.live_seed_mode, SeedMode::Inherit);
3943 crate::test_complete!("seed_plan_inherit_uses_canonical");
3944 }
3945
3946 #[test]
3947 fn seed_plan_override_uses_explicit_seed() {
3948 init_test("seed_plan_override_uses_explicit_seed");
3949 let plan = SeedPlan::inherit(0xBEEF, "test")
3950 .with_lab_override(0xCAFE)
3951 .with_live_override(0xFACE);
3952 assert_eq!(plan.effective_lab_seed(), 0xCAFE);
3953 assert_eq!(plan.effective_live_seed(), 0xFACE);
3954 assert_eq!(plan.lab_seed_mode, SeedMode::Override);
3955 assert_eq!(plan.live_seed_mode, SeedMode::Override);
3956 crate::test_complete!("seed_plan_override_uses_explicit_seed");
3957 }
3958
3959 #[test]
3960 fn seed_plan_override_without_value_falls_back_to_canonical() {
3961 init_test("seed_plan_override_without_value_falls_back");
3962 let mut plan = SeedPlan::inherit(0xBEEF, "test");
3963 plan.lab_seed_mode = SeedMode::Override;
3964 assert_eq!(plan.effective_lab_seed(), 0xBEEF);
3966 crate::test_complete!("seed_plan_override_without_value_falls_back");
3967 }
3968
3969 #[test]
3970 fn seed_plan_entropy_derives_from_effective() {
3971 init_test("seed_plan_entropy_derives_from_effective");
3972 let plan = SeedPlan::inherit(42, "test");
3973 let entropy = plan.effective_entropy_seed(42);
3974 assert_eq!(entropy, plan.effective_entropy_seed(42));
3976 assert_ne!(entropy, 42);
3978 crate::test_complete!("seed_plan_entropy_derives_from_effective");
3979 }
3980
3981 #[test]
3982 fn seed_plan_entropy_override() {
3983 init_test("seed_plan_entropy_override");
3984 let plan = SeedPlan::inherit(42, "test").with_entropy_seed(999);
3985 assert_eq!(plan.effective_entropy_seed(42), 999);
3986 assert_eq!(plan.effective_entropy_seed(100), 999);
3987 crate::test_complete!("seed_plan_entropy_override");
3988 }
3989
3990 #[test]
3991 fn seed_plan_to_lab_config() {
3992 init_test("seed_plan_to_lab_config");
3993 let plan = SeedPlan::inherit(0xDEAD, "test");
3994 let config = plan.to_lab_config();
3995 assert_eq!(config.seed, 0xDEAD);
3996 let expected_entropy = plan.effective_entropy_seed(0xDEAD);
3997 assert_eq!(config.entropy_seed, expected_entropy);
3998 crate::test_complete!("seed_plan_to_lab_config");
3999 }
4000
4001 #[test]
4002 fn seed_plan_to_lab_config_with_override() {
4003 init_test("seed_plan_to_lab_config_with_override");
4004 let plan = SeedPlan::inherit(0xDEAD, "test").with_lab_override(0xCAFE);
4005 let config = plan.to_lab_config();
4006 assert_eq!(config.seed, 0xCAFE);
4007 crate::test_complete!("seed_plan_to_lab_config_with_override");
4008 }
4009
4010 #[test]
4011 fn seed_plan_sweep_deterministic() {
4012 init_test("seed_plan_sweep_deterministic");
4013 let plan = SeedPlan::inherit(42, "test").with_replay_policy(ReplayPolicy::SeedSweep);
4014 let seeds1 = plan.sweep_seeds(5);
4015 let seeds2 = plan.sweep_seeds(5);
4016 assert_eq!(seeds1, seeds2);
4017 assert_eq!(seeds1.len(), 5);
4018 let mut unique = seeds1;
4020 unique.sort_unstable();
4021 unique.dedup();
4022 assert_eq!(unique.len(), 5);
4023 crate::test_complete!("seed_plan_sweep_deterministic");
4024 }
4025
4026 #[test]
4027 fn seed_plan_serde_roundtrip() {
4028 init_test("seed_plan_serde_roundtrip");
4029 let plan = SeedPlan::inherit(0xABCD, "lineage-1")
4030 .with_lab_override(0x1234)
4031 .with_entropy_seed(0x5678)
4032 .with_replay_policy(ReplayPolicy::SeedSweep);
4033 let json = serde_json::to_string_pretty(&plan).unwrap();
4034 let parsed: SeedPlan = serde_json::from_str(&json).unwrap();
4035 assert_eq!(parsed, plan);
4036 crate::test_complete!("seed_plan_serde_roundtrip");
4037 }
4038
4039 #[test]
4040 fn seed_plan_display() {
4041 init_test("seed_plan_display");
4042 let plan = SeedPlan::inherit(42, "test-scenario");
4043 let display = format!("{plan}");
4044 assert!(display.contains("0x2A"));
4045 assert!(display.contains("test-scenario"));
4046 crate::test_complete!("seed_plan_display");
4047 }
4048
4049 #[test]
4052 fn scenario_family_id_display() {
4053 init_test("scenario_family_id_display");
4054 let fam = ScenarioFamilyId::new("cancel.race", "cancellation.race", "v1");
4055 let s = format!("{fam}");
4056 assert!(s.contains("cancel.race"));
4057 assert!(s.contains("cancellation.race"));
4058 assert!(s.contains("v1"));
4059 crate::test_complete!("scenario_family_id_display");
4060 }
4061
4062 #[test]
4063 fn scenario_family_id_serde_roundtrip() {
4064 init_test("scenario_family_id_serde_roundtrip");
4065 let fam = ScenarioFamilyId::new("cancel.race", "cancellation.race", "v1");
4066 let json = serde_json::to_string(&fam).unwrap();
4067 let parsed: ScenarioFamilyId = serde_json::from_str(&json).unwrap();
4068 assert_eq!(parsed, fam);
4069 crate::test_complete!("scenario_family_id_serde_roundtrip");
4070 }
4071
4072 #[test]
4075 fn execution_instance_lab_vs_live() {
4076 init_test("execution_instance_lab_vs_live");
4077 let lab = ExecutionInstanceId::lab("test-family", 42);
4078 let live = ExecutionInstanceId::live("test-family", 42);
4079 assert_eq!(lab.runtime_kind, RuntimeKind::Lab);
4080 assert_eq!(live.runtime_kind, RuntimeKind::Live);
4081 assert_ne!(lab.key(), live.key());
4082 crate::test_complete!("execution_instance_lab_vs_live");
4083 }
4084
4085 #[test]
4086 fn execution_instance_key_stable() {
4087 init_test("execution_instance_key_stable");
4088 let inst = ExecutionInstanceId::lab("fam", 0xBEEF).with_run_index(3);
4089 let key1 = inst.key();
4090 let key2 = inst.key();
4091 assert_eq!(key1, key2);
4092 assert!(key1.contains("fam"));
4093 assert!(key1.contains("0xBEEF"));
4094 assert!(key1.contains('3'));
4095 crate::test_complete!("execution_instance_key_stable");
4096 }
4097
4098 #[test]
4101 fn runtime_kind_display() {
4102 init_test("runtime_kind_display");
4103 assert_eq!(format!("{}", RuntimeKind::Lab), "lab");
4104 assert_eq!(format!("{}", RuntimeKind::Live), "live");
4105 crate::test_complete!("runtime_kind_display");
4106 }
4107
4108 #[test]
4111 fn replay_metadata_lab_seeds_match_plan() {
4112 init_test("replay_metadata_lab_seeds_match_plan");
4113 let family = ScenarioFamilyId::new("test", "surface", "v1");
4114 let plan = SeedPlan::inherit(0xDEAD, "lineage");
4115 let meta = ReplayMetadata::for_lab(family, &plan);
4116 assert_eq!(meta.effective_seed, 0xDEAD);
4117 assert_eq!(meta.instance.runtime_kind, RuntimeKind::Lab);
4118 assert_eq!(
4119 meta.effective_entropy_seed,
4120 plan.effective_entropy_seed(0xDEAD)
4121 );
4122 crate::test_complete!("replay_metadata_lab_seeds_match_plan");
4123 }
4124
4125 #[test]
4126 fn replay_metadata_live_seeds_match_plan() {
4127 init_test("replay_metadata_live_seeds_match_plan");
4128 let family = ScenarioFamilyId::new("test", "surface", "v1");
4129 let plan = SeedPlan::inherit(0xCAFE, "lineage");
4130 let meta = ReplayMetadata::for_live(family, &plan);
4131 assert_eq!(meta.effective_seed, 0xCAFE);
4132 assert_eq!(meta.instance.runtime_kind, RuntimeKind::Live);
4133 crate::test_complete!("replay_metadata_live_seeds_match_plan");
4134 }
4135
4136 #[test]
4137 fn replay_metadata_with_overrides() {
4138 init_test("replay_metadata_with_overrides");
4139 let family = ScenarioFamilyId::new("test", "surface", "v1");
4140 let plan = SeedPlan::inherit(42, "lineage").with_lab_override(999);
4141 let meta = ReplayMetadata::for_lab(family, &plan);
4142 assert_eq!(meta.effective_seed, 999);
4143 crate::test_complete!("replay_metadata_with_overrides");
4144 }
4145
4146 #[test]
4147 fn replay_metadata_with_lab_report() {
4148 init_test("replay_metadata_with_lab_report");
4149 let family = ScenarioFamilyId::new("test", "surface", "v1");
4150 let plan = SeedPlan::inherit(42, "lineage");
4151 let meta = ReplayMetadata::for_lab(family, &plan)
4152 .with_lab_report(0xF1, 0xE1, 100, 0x51, 500)
4153 .with_repro_command("cargo test test -- --nocapture")
4154 .with_artifact_path("/tmp/artifacts/test");
4155 assert_eq!(meta.trace_fingerprint, Some(0xF1));
4156 assert_eq!(meta.event_count, Some(100));
4157 assert_eq!(meta.steps_total, Some(500));
4158 assert!(meta.repro_command.is_some());
4159 assert!(meta.artifact_path.is_some());
4160 crate::test_complete!("replay_metadata_with_lab_report");
4161 }
4162
4163 #[test]
4164 fn replay_metadata_default_repro_command() {
4165 init_test("replay_metadata_default_repro_command");
4166 let family = ScenarioFamilyId::new("cancel.race", "surface", "v1");
4167 let plan = SeedPlan::inherit(0xDEAD, "lineage");
4168 let meta = ReplayMetadata::for_lab(family, &plan);
4169 let cmd = meta.default_repro_command();
4170 assert!(cmd.contains("rch exec -- env ASUPERSYNC_SEED=0xDEAD"));
4171 assert!(cmd.contains("0xDEAD"));
4172 assert!(cmd.contains("cancel.race"));
4173 crate::test_complete!("replay_metadata_default_repro_command");
4174 }
4175
4176 #[test]
4177 fn replay_metadata_serde_roundtrip() {
4178 init_test("replay_metadata_serde_roundtrip");
4179 let family = ScenarioFamilyId::new("test", "surface", "v1");
4180 let plan = SeedPlan::inherit(42, "lineage");
4181 let meta = ReplayMetadata::for_lab(family, &plan).with_repro_command("cargo test");
4182 let json = serde_json::to_string_pretty(&meta).unwrap();
4183 let parsed: ReplayMetadata = serde_json::from_str(&json).unwrap();
4184 assert_eq!(parsed.effective_seed, meta.effective_seed);
4185 assert_eq!(parsed.family.id, "test");
4186 crate::test_complete!("replay_metadata_serde_roundtrip");
4187 }
4188
4189 #[test]
4192 fn seed_lineage_record_inherit_seeds_match() {
4193 init_test("seed_lineage_record_inherit_seeds_match");
4194 let plan = SeedPlan::inherit(0xBEEF, "lineage-1");
4195 let record = SeedLineageRecord::from_plan(&plan);
4196 assert!(record.seeds_match);
4197 assert_eq!(record.lab_effective_seed, 0xBEEF);
4198 assert_eq!(record.live_effective_seed, 0xBEEF);
4199 assert_eq!(record.lab_entropy_seed, record.live_entropy_seed);
4200 crate::test_complete!("seed_lineage_record_inherit_seeds_match");
4201 }
4202
4203 #[test]
4204 fn seed_lineage_record_override_seeds_differ() {
4205 init_test("seed_lineage_record_override_seeds_differ");
4206 let plan = SeedPlan::inherit(42, "lineage-1")
4207 .with_lab_override(100)
4208 .with_live_override(200);
4209 let record = SeedLineageRecord::from_plan(&plan);
4210 assert!(!record.seeds_match);
4211 assert_eq!(record.lab_effective_seed, 100);
4212 assert_eq!(record.live_effective_seed, 200);
4213 crate::test_complete!("seed_lineage_record_override_seeds_differ");
4214 }
4215
4216 #[test]
4217 fn seed_lineage_record_serde_roundtrip() {
4218 init_test("seed_lineage_record_serde_roundtrip");
4219 let plan = SeedPlan::inherit(42, "lin");
4220 let record = SeedLineageRecord::from_plan(&plan).with_annotation("source", "test");
4221 let json = serde_json::to_string(&record).unwrap();
4222 let parsed: SeedLineageRecord = serde_json::from_str(&json).unwrap();
4223 assert_eq!(parsed.canonical_seed, 42);
4224 assert_eq!(parsed.annotations.get("source").unwrap(), "test");
4225 crate::test_complete!("seed_lineage_record_serde_roundtrip");
4226 }
4227
4228 #[test]
4231 fn dual_run_scenario_identity_phase1() {
4232 init_test("dual_run_scenario_identity_phase1");
4233 let ident = DualRunScenarioIdentity::phase1(
4234 "phase1.cancel.race.one_loser",
4235 "cancellation.race",
4236 "v1",
4237 "Race two tasks, cancel loser, verify drain",
4238 42,
4239 );
4240 assert_eq!(ident.schema_version, DUAL_RUN_SCHEMA_VERSION);
4241 assert_eq!(ident.phase, Phase::Phase1);
4242 assert_eq!(ident.seed_plan.canonical_seed, 42);
4243 assert_eq!(
4244 ident.seed_plan.seed_lineage_id,
4245 "phase1.cancel.race.one_loser"
4246 );
4247 crate::test_complete!("dual_run_scenario_identity_phase1");
4248 }
4249
4250 #[test]
4251 fn dual_run_identity_lab_config() {
4252 init_test("dual_run_identity_lab_config");
4253 let ident = DualRunScenarioIdentity::phase1("test", "s", "v1", "d", 0xBEEF);
4254 let config = ident.to_lab_config();
4255 assert_eq!(config.seed, 0xBEEF);
4256 crate::test_complete!("dual_run_identity_lab_config");
4257 }
4258
4259 #[test]
4260 fn dual_run_identity_replay_metadata_lab_live_differ() {
4261 init_test("dual_run_identity_replay_metadata_lab_live_differ");
4262 let ident = DualRunScenarioIdentity::phase1("test", "s", "v1", "d", 42);
4263 let lab_meta = ident.lab_replay_metadata();
4264 let live_meta = ident.live_replay_metadata();
4265 assert_eq!(lab_meta.instance.runtime_kind, RuntimeKind::Lab);
4266 assert_eq!(live_meta.instance.runtime_kind, RuntimeKind::Live);
4267 assert_eq!(lab_meta.effective_seed, live_meta.effective_seed);
4269 crate::test_complete!("dual_run_identity_replay_metadata_lab_live_differ");
4270 }
4271
4272 #[test]
4273 fn dual_run_identity_family_id() {
4274 init_test("dual_run_identity_family_id");
4275 let ident = DualRunScenarioIdentity::phase1("test", "surface", "v1", "desc", 42);
4276 let fam = ident.family_id();
4277 assert_eq!(fam.id, "test");
4278 assert_eq!(fam.surface_id, "surface");
4279 assert_eq!(fam.surface_contract_version, "v1");
4280 crate::test_complete!("dual_run_identity_family_id");
4281 }
4282
4283 #[test]
4284 fn dual_run_identity_seed_lineage() {
4285 init_test("dual_run_identity_seed_lineage");
4286 let ident = DualRunScenarioIdentity::phase1("test", "s", "v1", "d", 42);
4287 let lineage = ident.seed_lineage();
4288 assert!(lineage.seeds_match);
4289 assert_eq!(lineage.canonical_seed, 42);
4290 crate::test_complete!("dual_run_identity_seed_lineage");
4291 }
4292
4293 #[test]
4294 fn dual_run_identity_with_seed_plan_override() {
4295 init_test("dual_run_identity_with_seed_plan_override");
4296 let plan = SeedPlan::inherit(99, "custom-lineage").with_lab_override(0xFF);
4297 let ident =
4298 DualRunScenarioIdentity::phase1("test", "s", "v1", "d", 42).with_seed_plan(plan);
4299 assert_eq!(ident.seed_plan.canonical_seed, 99);
4300 assert_eq!(ident.to_lab_config().seed, 0xFF);
4301 crate::test_complete!("dual_run_identity_with_seed_plan_override");
4302 }
4303
4304 #[test]
4305 fn dual_run_identity_metadata() {
4306 init_test("dual_run_identity_metadata");
4307 let ident = DualRunScenarioIdentity::phase1("test", "s", "v1", "d", 42)
4308 .with_metadata("bead", "2a6k9.2.3")
4309 .with_metadata("author", "SapphireHill");
4310 assert_eq!(ident.metadata.get("bead").unwrap(), "2a6k9.2.3");
4311 assert_eq!(ident.metadata.get("author").unwrap(), "SapphireHill");
4312 crate::test_complete!("dual_run_identity_metadata");
4313 }
4314
4315 #[test]
4316 fn dual_run_identity_serde_roundtrip() {
4317 init_test("dual_run_identity_serde_roundtrip");
4318 let ident = DualRunScenarioIdentity::phase1(
4319 "phase1.cancel.race.one_loser",
4320 "cancellation.race",
4321 "v1",
4322 "Race two tasks, cancel loser, verify drain",
4323 42,
4324 )
4325 .with_metadata("bead", "2a6k9.2.3");
4326 let json = serde_json::to_string_pretty(&ident).unwrap();
4327 let parsed: DualRunScenarioIdentity = serde_json::from_str(&json).unwrap();
4328 assert_eq!(parsed.scenario_id, ident.scenario_id);
4329 assert_eq!(parsed.seed_plan, ident.seed_plan);
4330 assert_eq!(parsed.phase, Phase::Phase1);
4331 crate::test_complete!("dual_run_identity_serde_roundtrip");
4332 }
4333
4334 #[test]
4337 fn same_plan_produces_same_lab_config() {
4338 init_test("same_plan_produces_same_lab_config");
4339 let plan = SeedPlan::inherit(0xCAFE_BABE, "determinism-check");
4340 let c1 = plan.to_lab_config();
4341 let c2 = plan.to_lab_config();
4342 assert_eq!(c1.seed, c2.seed);
4343 assert_eq!(c1.entropy_seed, c2.entropy_seed);
4344 crate::test_complete!("same_plan_produces_same_lab_config");
4345 }
4346
4347 #[test]
4348 fn inherit_mode_lab_live_seeds_identical() {
4349 init_test("inherit_mode_lab_live_seeds_identical");
4350 let plan = SeedPlan::inherit(0xDEAD_BEEF, "identical-check");
4351 assert_eq!(plan.effective_lab_seed(), plan.effective_live_seed());
4352 let lab_ent = plan.effective_entropy_seed(plan.effective_lab_seed());
4353 let live_ent = plan.effective_entropy_seed(plan.effective_live_seed());
4354 assert_eq!(lab_ent, live_ent);
4355 crate::test_complete!("inherit_mode_lab_live_seeds_identical");
4356 }
4357
4358 #[test]
4359 fn different_canonical_seeds_produce_different_entropies() {
4360 init_test("different_canonical_seeds_different_entropies");
4361 let p1 = SeedPlan::inherit(1, "a");
4362 let p2 = SeedPlan::inherit(2, "b");
4363 assert_ne!(
4364 p1.effective_entropy_seed(p1.effective_lab_seed()),
4365 p2.effective_entropy_seed(p2.effective_lab_seed())
4366 );
4367 crate::test_complete!("different_canonical_seeds_different_entropies");
4368 }
4369
4370 fn make_happy_semantics() -> NormalizedSemantics {
4373 NormalizedSemantics {
4374 terminal_outcome: TerminalOutcome::ok(),
4375 cancellation: CancellationRecord::none(),
4376 loser_drain: LoserDrainRecord::not_applicable(),
4377 region_close: RegionCloseRecord::quiescent(),
4378 obligation_balance: ObligationBalanceRecord::zero(),
4379 resource_surface: ResourceSurfaceRecord::empty("test"),
4380 }
4381 }
4382
4383 fn make_observable(kind: RuntimeKind, semantics: NormalizedSemantics) -> NormalizedObservable {
4384 let ident = DualRunScenarioIdentity::phase1("test", "s", "v1", "d", 42);
4385 let prov = match kind {
4386 RuntimeKind::Lab => ident.lab_replay_metadata(),
4387 RuntimeKind::Live => ident.live_replay_metadata(),
4388 };
4389 NormalizedObservable::new(&ident, kind, semantics, prov)
4390 }
4391
4392 #[test]
4393 fn terminal_outcome_ok_serde() {
4394 init_test("terminal_outcome_ok_serde");
4395 let t = TerminalOutcome::ok();
4396 let json = serde_json::to_string(&t).unwrap();
4397 let parsed: TerminalOutcome = serde_json::from_str(&json).unwrap();
4398 assert_eq!(parsed.class, OutcomeClass::Ok);
4399 crate::test_complete!("terminal_outcome_ok_serde");
4400 }
4401
4402 #[test]
4403 fn terminal_outcome_cancelled() {
4404 init_test("terminal_outcome_cancelled");
4405 let t = TerminalOutcome::cancelled("user_request");
4406 assert_eq!(t.class, OutcomeClass::Cancelled);
4407 assert_eq!(t.cancel_reason_class.as_deref(), Some("user_request"));
4408 crate::test_complete!("terminal_outcome_cancelled");
4409 }
4410
4411 #[test]
4412 fn cancellation_record_none_vs_completed() {
4413 init_test("cancellation_record_none_vs_completed");
4414 let none = CancellationRecord::none();
4415 let completed = CancellationRecord::completed();
4416 assert!(!none.requested);
4417 assert!(completed.requested);
4418 assert!(completed.acknowledged);
4419 assert!(completed.cleanup_completed);
4420 assert!(completed.finalization_completed);
4421 assert_eq!(completed.terminal_phase, CancelTerminalPhase::Completed);
4422 crate::test_complete!("cancellation_record_none_vs_completed");
4423 }
4424
4425 #[test]
4426 fn loser_drain_complete() {
4427 init_test("loser_drain_complete");
4428 let drain = LoserDrainRecord::complete(3);
4429 assert!(drain.applicable);
4430 assert_eq!(drain.expected_losers, 3);
4431 assert_eq!(drain.drained_losers, 3);
4432 assert_eq!(drain.status, DrainStatus::Complete);
4433 crate::test_complete!("loser_drain_complete");
4434 }
4435
4436 #[test]
4437 fn obligation_balance_recompute() {
4438 init_test("obligation_balance_recompute");
4439 let b = ObligationBalanceRecord {
4440 reserved: 10,
4441 committed: 7,
4442 aborted: 2,
4443 leaked: 1,
4444 unresolved: 99, balanced: true, }
4447 .recompute();
4448 assert_eq!(b.unresolved, 0); assert!(!b.balanced); crate::test_complete!("obligation_balance_recompute");
4451 }
4452
4453 #[test]
4454 fn resource_surface_counter_tolerance() {
4455 init_test("resource_surface_counter_tolerance");
4456 let rs = ResourceSurfaceRecord::empty("test-surface")
4457 .with_counter("msgs", 5)
4458 .with_counter_tolerance("bytes", 100, CounterTolerance::AtLeast);
4459 assert_eq!(rs.counters["msgs"], 5);
4460 assert_eq!(rs.tolerances["msgs"], CounterTolerance::Exact);
4461 assert_eq!(rs.tolerances["bytes"], CounterTolerance::AtLeast);
4462 crate::test_complete!("resource_surface_counter_tolerance");
4463 }
4464
4465 #[test]
4466 fn normalized_observable_serde_roundtrip() {
4467 init_test("normalized_observable_serde_roundtrip");
4468 let obs = make_observable(RuntimeKind::Lab, make_happy_semantics());
4469 let json = serde_json::to_string_pretty(&obs).unwrap();
4470 let parsed: NormalizedObservable = serde_json::from_str(&json).unwrap();
4471 assert_eq!(parsed.schema_version, NORMALIZED_OBSERVABLE_SCHEMA_VERSION);
4472 assert_eq!(parsed.runtime_kind, RuntimeKind::Lab);
4473 assert_eq!(parsed.semantics.terminal_outcome.class, OutcomeClass::Ok);
4474 crate::test_complete!("normalized_observable_serde_roundtrip");
4475 }
4476
4477 #[test]
4480 fn compare_identical_observables_passes() {
4481 init_test("compare_identical_observables_passes");
4482 let lab = make_observable(RuntimeKind::Lab, make_happy_semantics());
4483 let live = make_observable(RuntimeKind::Live, make_happy_semantics());
4484 let plan = SeedPlan::inherit(42, "test");
4485 let verdict = compare_observables(&lab, &live, SeedLineageRecord::from_plan(&plan));
4486 assert!(verdict.passed);
4487 assert!(verdict.mismatches.is_empty());
4488 crate::test_complete!("compare_identical_observables_passes");
4489 }
4490
4491 #[test]
4492 fn compare_outcome_mismatch_fails() {
4493 init_test("compare_outcome_mismatch_fails");
4494 let lab_sem = make_happy_semantics();
4495 let mut live_sem = make_happy_semantics();
4496 live_sem.terminal_outcome = TerminalOutcome::cancelled("timeout");
4497 let lab = make_observable(RuntimeKind::Lab, lab_sem);
4498 let live = make_observable(RuntimeKind::Live, live_sem);
4499 let plan = SeedPlan::inherit(42, "test");
4500 let verdict = compare_observables(&lab, &live, SeedLineageRecord::from_plan(&plan));
4501 assert!(!verdict.passed);
4502 assert!(
4503 verdict
4504 .mismatches
4505 .iter()
4506 .any(|m| m.field.contains("terminal_outcome.class"))
4507 );
4508 crate::test_complete!("compare_outcome_mismatch_fails");
4509 }
4510
4511 #[test]
4512 fn compare_surface_identity_mismatch_fails() {
4513 init_test("compare_surface_identity_mismatch_fails");
4514 let lab = make_observable(RuntimeKind::Lab, make_happy_semantics());
4515 let mut live = make_observable(RuntimeKind::Live, make_happy_semantics());
4516 live.surface_id = "different.surface".to_string();
4517 live.surface_contract_version = "v2".to_string();
4518 let plan = SeedPlan::inherit(42, "test");
4519 let verdict = compare_observables(&lab, &live, SeedLineageRecord::from_plan(&plan));
4520 assert!(!verdict.passed);
4521 assert!(verdict.mismatches.iter().any(|m| m.field == "surface_id"));
4522 assert!(
4523 verdict
4524 .mismatches
4525 .iter()
4526 .any(|m| m.field == "surface_contract_version")
4527 );
4528 crate::test_complete!("compare_surface_identity_mismatch_fails");
4529 }
4530
4531 #[test]
4532 fn compare_terminal_reason_and_panic_class_mismatch_fails() {
4533 init_test("compare_terminal_reason_and_panic_class_mismatch_fails");
4534 let mut lab_sem = make_happy_semantics();
4535 lab_sem.terminal_outcome = TerminalOutcome::cancelled("timeout");
4536
4537 let mut live_sem = make_happy_semantics();
4538 live_sem.terminal_outcome = TerminalOutcome::cancelled("shutdown");
4539
4540 let lab = make_observable(RuntimeKind::Lab, lab_sem.clone());
4541 let live = make_observable(RuntimeKind::Live, live_sem);
4542 let plan = SeedPlan::inherit(42, "test");
4543 let verdict = compare_observables(&lab, &live, SeedLineageRecord::from_plan(&plan));
4544 assert!(!verdict.passed);
4545 assert!(
4546 verdict
4547 .mismatches
4548 .iter()
4549 .any(|m| { m.field == "semantics.terminal_outcome.cancel_reason_class" })
4550 );
4551
4552 let mut panic_sem = lab_sem;
4553 panic_sem.terminal_outcome = TerminalOutcome {
4554 class: OutcomeClass::Panicked,
4555 severity: OutcomeClass::Panicked,
4556 surface_result: None,
4557 error_class: None,
4558 cancel_reason_class: None,
4559 panic_class: Some("panic_a".to_string()),
4560 };
4561 let mut other_panic_sem = make_happy_semantics();
4562 other_panic_sem.terminal_outcome = TerminalOutcome {
4563 class: OutcomeClass::Panicked,
4564 severity: OutcomeClass::Panicked,
4565 surface_result: None,
4566 error_class: None,
4567 cancel_reason_class: None,
4568 panic_class: Some("panic_b".to_string()),
4569 };
4570 let panic_lab = make_observable(RuntimeKind::Lab, panic_sem);
4571 let panic_live = make_observable(RuntimeKind::Live, other_panic_sem);
4572 let panic_verdict =
4573 compare_observables(&panic_lab, &panic_live, SeedLineageRecord::from_plan(&plan));
4574 assert!(!panic_verdict.passed);
4575 assert!(
4576 panic_verdict
4577 .mismatches
4578 .iter()
4579 .any(|m| m.field == "semantics.terminal_outcome.panic_class")
4580 );
4581 crate::test_complete!("compare_terminal_reason_and_panic_class_mismatch_fails");
4582 }
4583
4584 #[test]
4585 fn compare_obligation_leak_mismatch() {
4586 init_test("compare_obligation_leak_mismatch");
4587 let lab_sem = make_happy_semantics();
4588 let mut live_sem = make_happy_semantics();
4589 live_sem.obligation_balance = ObligationBalanceRecord {
4590 reserved: 5,
4591 committed: 3,
4592 aborted: 0,
4593 leaked: 2,
4594 unresolved: 0,
4595 balanced: false,
4596 };
4597 let lab = make_observable(RuntimeKind::Lab, lab_sem);
4598 let live = make_observable(RuntimeKind::Live, live_sem);
4599 let plan = SeedPlan::inherit(42, "test");
4600 let verdict = compare_observables(&lab, &live, SeedLineageRecord::from_plan(&plan));
4601 assert!(!verdict.passed);
4602 assert!(
4603 verdict
4604 .mismatches
4605 .iter()
4606 .any(|m| m.field.contains("leaked"))
4607 );
4608 crate::test_complete!("compare_obligation_leak_mismatch");
4609 }
4610
4611 #[test]
4612 fn compare_obligation_component_mismatch_fails_even_when_balanced() {
4613 init_test("compare_obligation_component_mismatch_fails_even_when_balanced");
4614 let mut lab_sem = make_happy_semantics();
4615 lab_sem.obligation_balance = ObligationBalanceRecord::balanced(3, 3, 0);
4616 let mut live_sem = make_happy_semantics();
4617 live_sem.obligation_balance = ObligationBalanceRecord::balanced(3, 2, 1);
4618 let lab = make_observable(RuntimeKind::Lab, lab_sem);
4619 let live = make_observable(RuntimeKind::Live, live_sem);
4620 let plan = SeedPlan::inherit(42, "test");
4621 let verdict = compare_observables(&lab, &live, SeedLineageRecord::from_plan(&plan));
4622 assert!(!verdict.passed);
4623 assert!(
4624 verdict
4625 .mismatches
4626 .iter()
4627 .any(|m| m.field == "semantics.obligation_balance.committed")
4628 );
4629 assert!(
4630 verdict
4631 .mismatches
4632 .iter()
4633 .any(|m| m.field == "semantics.obligation_balance.aborted")
4634 );
4635 crate::test_complete!("compare_obligation_component_mismatch_fails_even_when_balanced");
4636 }
4637
4638 #[test]
4639 fn compare_resource_counter_exact_mismatch() {
4640 init_test("compare_resource_counter_exact_mismatch");
4641 let mut lab_sem = make_happy_semantics();
4642 lab_sem.resource_surface = ResourceSurfaceRecord::empty("test").with_counter("msgs", 5);
4643 let mut live_sem = make_happy_semantics();
4644 live_sem.resource_surface = ResourceSurfaceRecord::empty("test").with_counter("msgs", 3);
4645 let lab = make_observable(RuntimeKind::Lab, lab_sem);
4646 let live = make_observable(RuntimeKind::Live, live_sem);
4647 let plan = SeedPlan::inherit(42, "test");
4648 let verdict = compare_observables(&lab, &live, SeedLineageRecord::from_plan(&plan));
4649 assert!(!verdict.passed);
4650 assert!(
4651 verdict
4652 .mismatches
4653 .iter()
4654 .any(|m| m.field.contains("counters.msgs"))
4655 );
4656 crate::test_complete!("compare_resource_counter_exact_mismatch");
4657 }
4658
4659 #[test]
4660 fn compare_resource_counter_missing_in_live_fails() {
4661 init_test("compare_resource_counter_missing_in_live_fails");
4662 let mut lab_sem = make_happy_semantics();
4663 lab_sem.resource_surface = ResourceSurfaceRecord::empty("test").with_counter("msgs", 0);
4664 let mut live_sem = make_happy_semantics();
4665 live_sem.resource_surface = ResourceSurfaceRecord::empty("test");
4666 let lab = make_observable(RuntimeKind::Lab, lab_sem);
4667 let live = make_observable(RuntimeKind::Live, live_sem);
4668 let plan = SeedPlan::inherit(42, "test");
4669 let verdict = compare_observables(&lab, &live, SeedLineageRecord::from_plan(&plan));
4670 assert!(!verdict.passed);
4671 assert!(
4672 verdict
4673 .mismatches
4674 .iter()
4675 .any(|m| m.description.contains("missing in live observable"))
4676 );
4677 crate::test_complete!("compare_resource_counter_missing_in_live_fails");
4678 }
4679
4680 #[test]
4681 fn compare_resource_counter_missing_in_lab_fails() {
4682 init_test("compare_resource_counter_missing_in_lab_fails");
4683 let mut lab_sem = make_happy_semantics();
4684 lab_sem.resource_surface = ResourceSurfaceRecord::empty("test");
4685 let mut live_sem = make_happy_semantics();
4686 live_sem.resource_surface = ResourceSurfaceRecord::empty("test").with_counter("msgs", 0);
4687 let lab = make_observable(RuntimeKind::Lab, lab_sem);
4688 let live = make_observable(RuntimeKind::Live, live_sem);
4689 let plan = SeedPlan::inherit(42, "test");
4690 let verdict = compare_observables(&lab, &live, SeedLineageRecord::from_plan(&plan));
4691 assert!(!verdict.passed);
4692 assert!(
4693 verdict
4694 .mismatches
4695 .iter()
4696 .any(|m| m.description.contains("present in live but not in lab"))
4697 );
4698 crate::test_complete!("compare_resource_counter_missing_in_lab_fails");
4699 }
4700
4701 #[test]
4702 fn compare_resource_tolerance_mismatch_fails() {
4703 init_test("compare_resource_tolerance_mismatch_fails");
4704 let mut lab_sem = make_happy_semantics();
4705 lab_sem.resource_surface = ResourceSurfaceRecord::empty("test").with_counter_tolerance(
4706 "msgs",
4707 5,
4708 CounterTolerance::Exact,
4709 );
4710 let mut live_sem = make_happy_semantics();
4711 live_sem.resource_surface = ResourceSurfaceRecord::empty("test").with_counter_tolerance(
4712 "msgs",
4713 5,
4714 CounterTolerance::Unsupported,
4715 );
4716 let lab = make_observable(RuntimeKind::Lab, lab_sem);
4717 let live = make_observable(RuntimeKind::Live, live_sem);
4718 let plan = SeedPlan::inherit(42, "test");
4719 let verdict = compare_observables(&lab, &live, SeedLineageRecord::from_plan(&plan));
4720 assert!(!verdict.passed);
4721 assert!(
4722 verdict
4723 .mismatches
4724 .iter()
4725 .any(|m| m.field == "semantics.resource_surface.tolerances.msgs")
4726 );
4727 crate::test_complete!("compare_resource_tolerance_mismatch_fails");
4728 }
4729
4730 #[test]
4731 fn compare_resource_counter_at_least_passes() {
4732 init_test("compare_resource_counter_at_least_passes");
4733 let mut lab_sem = make_happy_semantics();
4734 lab_sem.resource_surface = ResourceSurfaceRecord::empty("test").with_counter_tolerance(
4735 "msgs",
4736 5,
4737 CounterTolerance::AtLeast,
4738 );
4739 let mut live_sem = make_happy_semantics();
4740 live_sem.resource_surface = ResourceSurfaceRecord::empty("test").with_counter_tolerance(
4741 "msgs",
4742 7,
4743 CounterTolerance::AtLeast,
4744 );
4745 let lab = make_observable(RuntimeKind::Lab, lab_sem);
4746 let live = make_observable(RuntimeKind::Live, live_sem);
4747 let plan = SeedPlan::inherit(42, "test");
4748 let verdict = compare_observables(&lab, &live, SeedLineageRecord::from_plan(&plan));
4749 assert!(verdict.passed);
4750 crate::test_complete!("compare_resource_counter_at_least_passes");
4751 }
4752
4753 #[test]
4754 fn compare_region_close_counts_mismatch_fails() {
4755 init_test("compare_region_close_counts_mismatch_fails");
4756 let lab_sem = make_happy_semantics();
4757 let mut live_sem = make_happy_semantics();
4758 live_sem.region_close.live_children = 1;
4759 live_sem.region_close.finalizers_pending = 2;
4760 let lab = make_observable(RuntimeKind::Lab, lab_sem);
4761 let live = make_observable(RuntimeKind::Live, live_sem);
4762 let plan = SeedPlan::inherit(42, "test");
4763 let verdict = compare_observables(&lab, &live, SeedLineageRecord::from_plan(&plan));
4764 assert!(!verdict.passed);
4765 assert!(
4766 verdict
4767 .mismatches
4768 .iter()
4769 .any(|m| m.field == "semantics.region_close.live_children")
4770 );
4771 assert!(
4772 verdict
4773 .mismatches
4774 .iter()
4775 .any(|m| m.field == "semantics.region_close.finalizers_pending")
4776 );
4777 crate::test_complete!("compare_region_close_counts_mismatch_fails");
4778 }
4779
4780 #[test]
4781 fn compare_region_close_ignores_non_quiescent_root_state_hint_mismatch() {
4782 init_test("compare_region_close_ignores_non_quiescent_root_state_hint_mismatch");
4783 let mut lab_sem = make_happy_semantics();
4784 lab_sem.region_close = RegionCloseRecord {
4785 root_state: RegionState::Open,
4786 quiescent: false,
4787 live_children: 0,
4788 finalizers_pending: 0,
4789 close_completed: false,
4790 };
4791
4792 let mut live_sem = make_happy_semantics();
4793 live_sem.region_close = RegionCloseRecord {
4794 root_state: RegionState::Finalizing,
4795 quiescent: false,
4796 live_children: 0,
4797 finalizers_pending: 0,
4798 close_completed: false,
4799 };
4800
4801 let lab = make_observable(RuntimeKind::Lab, lab_sem);
4802 let live = make_observable(RuntimeKind::Live, live_sem);
4803 let plan = SeedPlan::inherit(42, "test");
4804 let verdict = compare_observables(&lab, &live, SeedLineageRecord::from_plan(&plan));
4805
4806 assert!(verdict.passed);
4807 assert!(
4808 !verdict
4809 .mismatches
4810 .iter()
4811 .any(|m| m.field == "semantics.region_close.root_state")
4812 );
4813 crate::test_complete!(
4814 "compare_region_close_ignores_non_quiescent_root_state_hint_mismatch"
4815 );
4816 }
4817
4818 #[test]
4819 fn compare_region_close_ignores_unknown_non_quiescent_lab_counts() {
4820 init_test("compare_region_close_ignores_unknown_non_quiescent_lab_counts");
4821 let mut lab_sem = make_happy_semantics();
4822 lab_sem.region_close = RegionCloseRecord {
4823 root_state: RegionState::Closing,
4824 quiescent: false,
4825 live_children: 0,
4826 finalizers_pending: 0,
4827 close_completed: false,
4828 };
4829
4830 let mut live_sem = make_happy_semantics();
4831 live_sem.region_close = RegionCloseRecord {
4832 root_state: RegionState::Draining,
4833 quiescent: false,
4834 live_children: 1,
4835 finalizers_pending: 0,
4836 close_completed: false,
4837 };
4838
4839 let lab = make_observable(RuntimeKind::Lab, lab_sem);
4840 let live = make_observable(RuntimeKind::Live, live_sem);
4841 let plan = SeedPlan::inherit(42, "test");
4842 let verdict = compare_observables(&lab, &live, SeedLineageRecord::from_plan(&plan));
4843
4844 assert!(verdict.passed);
4845 crate::test_complete!("compare_region_close_ignores_unknown_non_quiescent_lab_counts");
4846 }
4847
4848 #[test]
4849 fn compare_loser_drain_ignores_unknown_lab_counts_from_oracle_pass() {
4850 init_test("compare_loser_drain_ignores_unknown_lab_counts_from_oracle_pass");
4851 let mut lab_sem = make_happy_semantics();
4852 lab_sem.loser_drain = LoserDrainRecord {
4853 applicable: true,
4854 expected_losers: 0,
4855 drained_losers: 0,
4856 status: DrainStatus::Complete,
4857 evidence: Some("oracle.loser_drain.passed".to_string()),
4858 };
4859
4860 let mut live_sem = make_happy_semantics();
4861 live_sem.loser_drain = LoserDrainRecord::complete(2);
4862
4863 let lab = make_observable(RuntimeKind::Lab, lab_sem);
4864 let live = make_observable(RuntimeKind::Live, live_sem);
4865 let plan = SeedPlan::inherit(42, "test");
4866 let verdict = compare_observables(&lab, &live, SeedLineageRecord::from_plan(&plan));
4867
4868 assert!(verdict.passed);
4869 crate::test_complete!("compare_loser_drain_ignores_unknown_lab_counts_from_oracle_pass");
4870 }
4871
4872 #[test]
4873 fn compare_loser_drain_unknown_lab_counts_still_fail_on_status_mismatch() {
4874 init_test("compare_loser_drain_unknown_lab_counts_still_fail_on_status_mismatch");
4875 let mut lab_sem = make_happy_semantics();
4876 lab_sem.loser_drain = LoserDrainRecord {
4877 applicable: true,
4878 expected_losers: 0,
4879 drained_losers: 0,
4880 status: DrainStatus::Complete,
4881 evidence: Some("oracle.loser_drain.passed".to_string()),
4882 };
4883
4884 let mut live_sem = make_happy_semantics();
4885 live_sem.loser_drain = LoserDrainRecord {
4886 applicable: true,
4887 expected_losers: 2,
4888 drained_losers: 1,
4889 status: DrainStatus::Incomplete,
4890 evidence: Some("task_handle.join".to_string()),
4891 };
4892
4893 let lab = make_observable(RuntimeKind::Lab, lab_sem);
4894 let live = make_observable(RuntimeKind::Live, live_sem);
4895 let plan = SeedPlan::inherit(42, "test");
4896 let verdict = compare_observables(&lab, &live, SeedLineageRecord::from_plan(&plan));
4897
4898 assert!(!verdict.passed);
4899 assert!(
4900 verdict
4901 .mismatches
4902 .iter()
4903 .any(|m| m.field == "semantics.loser_drain.status")
4904 );
4905 crate::test_complete!(
4906 "compare_loser_drain_unknown_lab_counts_still_fail_on_status_mismatch"
4907 );
4908 }
4909
4910 #[test]
4911 fn compare_cancellation_mismatch() {
4912 init_test("compare_cancellation_mismatch");
4913 let mut lab_sem = make_happy_semantics();
4914 lab_sem.cancellation = CancellationRecord::completed();
4915 let live_sem = make_happy_semantics(); let lab = make_observable(RuntimeKind::Lab, lab_sem);
4917 let live = make_observable(RuntimeKind::Live, live_sem);
4918 let plan = SeedPlan::inherit(42, "test");
4919 let verdict = compare_observables(&lab, &live, SeedLineageRecord::from_plan(&plan));
4920 assert!(!verdict.passed);
4921 assert!(
4922 verdict
4923 .mismatches
4924 .iter()
4925 .any(|m| m.field.contains("cancellation"))
4926 );
4927 crate::test_complete!("compare_cancellation_mismatch");
4928 }
4929
4930 #[test]
4931 fn verdict_display_pass() {
4932 init_test("verdict_display_pass");
4933 let lab = make_observable(RuntimeKind::Lab, make_happy_semantics());
4934 let live = make_observable(RuntimeKind::Live, make_happy_semantics());
4935 let plan = SeedPlan::inherit(42, "test");
4936 let verdict = compare_observables(&lab, &live, SeedLineageRecord::from_plan(&plan));
4937 let summary = verdict.summary();
4938 assert!(summary.contains("PASS"));
4939 crate::test_complete!("verdict_display_pass");
4940 }
4941
4942 #[test]
4943 fn verdict_display_fail() {
4944 init_test("verdict_display_fail");
4945 let lab_sem = make_happy_semantics();
4946 let mut live_sem = make_happy_semantics();
4947 live_sem.region_close.quiescent = false;
4948 let lab = make_observable(RuntimeKind::Lab, lab_sem);
4949 let live = make_observable(RuntimeKind::Live, live_sem);
4950 let plan = SeedPlan::inherit(42, "test");
4951 let verdict = compare_observables(&lab, &live, SeedLineageRecord::from_plan(&plan));
4952 let summary = verdict.summary();
4953 assert!(summary.contains("FAIL"));
4954 assert!(summary.contains("mismatch"));
4955 crate::test_complete!("verdict_display_fail");
4956 }
4957
4958 #[test]
4961 fn check_core_invariants_all_pass() {
4962 init_test("check_core_invariants_all_pass");
4963 let obs = make_observable(RuntimeKind::Lab, make_happy_semantics());
4964 let violations = check_core_invariants(&obs);
4965 assert!(violations.is_empty());
4966 crate::test_complete!("check_core_invariants_all_pass");
4967 }
4968
4969 #[test]
4970 fn check_core_invariants_obligation_leak() {
4971 init_test("check_core_invariants_obligation_leak");
4972 let mut sem = make_happy_semantics();
4973 sem.obligation_balance.leaked = 1;
4974 sem.obligation_balance.balanced = false;
4975 let obs = make_observable(RuntimeKind::Lab, sem);
4976 let violations = check_core_invariants(&obs);
4977 assert!(!violations.is_empty());
4978 assert!(violations[0].contains("leaked"));
4979 crate::test_complete!("check_core_invariants_obligation_leak");
4980 }
4981
4982 #[test]
4983 fn check_core_invariants_not_quiescent() {
4984 init_test("check_core_invariants_not_quiescent");
4985 let mut sem = make_happy_semantics();
4986 sem.region_close.quiescent = false;
4987 sem.region_close.live_children = 2;
4988 let obs = make_observable(RuntimeKind::Lab, sem);
4989 let violations = check_core_invariants(&obs);
4990 assert!(violations.iter().any(|v| v.contains("quiescent")));
4991 crate::test_complete!("check_core_invariants_not_quiescent");
4992 }
4993
4994 #[test]
4995 fn check_core_invariants_incomplete_drain() {
4996 init_test("check_core_invariants_incomplete_drain");
4997 let mut sem = make_happy_semantics();
4998 sem.loser_drain = LoserDrainRecord {
4999 applicable: true,
5000 expected_losers: 3,
5001 drained_losers: 1,
5002 status: DrainStatus::Incomplete,
5003 evidence: None,
5004 };
5005 let obs = make_observable(RuntimeKind::Lab, sem);
5006 let violations = check_core_invariants(&obs);
5007 assert!(violations.iter().any(|v| v.contains("drain")));
5008 crate::test_complete!("check_core_invariants_incomplete_drain");
5009 }
5010
5011 #[test]
5012 fn check_core_invariants_cancel_incomplete() {
5013 init_test("check_core_invariants_cancel_incomplete");
5014 let mut sem = make_happy_semantics();
5015 sem.cancellation.requested = true;
5016 sem.cancellation.cleanup_completed = false;
5017 sem.cancellation.terminal_phase = CancelTerminalPhase::Cancelling;
5018 let obs = make_observable(RuntimeKind::Lab, sem);
5019 let violations = check_core_invariants(&obs);
5020 assert!(
5021 violations
5022 .iter()
5023 .any(|v| v.contains("Cancellation cleanup incomplete"))
5024 );
5025 assert!(
5026 !violations
5027 .iter()
5028 .any(|v| v.contains("Cancellation finalization incomplete")),
5029 "finalization should not be required before cleanup completes"
5030 );
5031 crate::test_complete!("check_core_invariants_cancel_incomplete");
5032 }
5033
5034 #[test]
5035 fn check_core_invariants_cancel_finalization_incomplete() {
5036 init_test("check_core_invariants_cancel_finalization_incomplete");
5037 let mut sem = make_happy_semantics();
5038 sem.cancellation.requested = true;
5039 sem.cancellation.cleanup_completed = true;
5040 sem.cancellation.finalization_completed = false;
5041 sem.cancellation.terminal_phase = CancelTerminalPhase::Finalizing;
5042 let obs = make_observable(RuntimeKind::Lab, sem);
5043 let violations = check_core_invariants(&obs);
5044 assert!(
5045 violations
5046 .iter()
5047 .any(|v| v.contains("Cancellation finalization incomplete"))
5048 );
5049 crate::test_complete!("check_core_invariants_cancel_finalization_incomplete");
5050 }
5051
5052 #[test]
5055 fn assert_semantics_identical_passes() {
5056 init_test("assert_semantics_identical_passes");
5057 let sem = make_happy_semantics();
5058 let mismatches = assert_semantics(&sem, &sem);
5059 assert!(mismatches.is_empty());
5060 crate::test_complete!("assert_semantics_identical_passes");
5061 }
5062
5063 #[test]
5064 fn assert_semantics_detects_diff() {
5065 init_test("assert_semantics_detects_diff");
5066 let expected = make_happy_semantics();
5067 let mut actual = make_happy_semantics();
5068 actual.terminal_outcome = TerminalOutcome::err("network_error");
5069 let mismatches = assert_semantics(&actual, &expected);
5070 assert!(!mismatches.is_empty());
5071 crate::test_complete!("assert_semantics_detects_diff");
5072 }
5073
5074 #[test]
5077 fn harness_identical_runs_pass() {
5078 init_test("harness_identical_runs_pass");
5079 let result = DualRunHarness::phase1(
5080 "test.happy_path",
5081 "test.surface",
5082 "v1",
5083 "Both sides produce identical semantics",
5084 42,
5085 )
5086 .lab(|_config| make_happy_semantics())
5087 .live(|_seed, _entropy| make_happy_semantics())
5088 .run();
5089
5090 assert!(result.passed());
5091 assert!(result.verdict.is_equivalent());
5092 assert!(result.lab_invariant_violations.is_empty());
5093 assert!(result.live_invariant_violations.is_empty());
5094 crate::test_complete!("harness_identical_runs_pass");
5095 }
5096
5097 #[test]
5098 fn harness_outcome_mismatch_fails() {
5099 init_test("harness_outcome_mismatch_fails");
5100 let result = DualRunHarness::phase1(
5101 "test.mismatch",
5102 "test.surface",
5103 "v1",
5104 "Lab succeeds, live cancels",
5105 42,
5106 )
5107 .lab(|_config| make_happy_semantics())
5108 .live(|_seed, _entropy| {
5109 let mut sem = make_happy_semantics();
5110 sem.terminal_outcome = TerminalOutcome::cancelled("timeout");
5111 sem
5112 })
5113 .run();
5114
5115 assert!(!result.passed());
5116 assert!(!result.verdict.is_equivalent());
5117 crate::test_complete!("harness_outcome_mismatch_fails");
5118 }
5119
5120 #[test]
5121 fn harness_lab_invariant_violation_fails() {
5122 init_test("harness_lab_invariant_violation_fails");
5123 let result = DualRunHarness::phase1(
5124 "test.leak",
5125 "test.surface",
5126 "v1",
5127 "Lab leaks obligations",
5128 42,
5129 )
5130 .lab(|_config| {
5131 let mut sem = make_happy_semantics();
5132 sem.obligation_balance.leaked = 1;
5133 sem.obligation_balance.balanced = false;
5134 sem
5135 })
5136 .live(|_seed, _entropy| {
5137 let mut sem = make_happy_semantics();
5138 sem.obligation_balance.leaked = 1;
5139 sem.obligation_balance.balanced = false;
5140 sem
5141 })
5142 .run();
5143
5144 assert!(result.verdict.is_equivalent());
5146 assert!(!result.lab_invariant_violations.is_empty());
5147 assert!(!result.passed()); crate::test_complete!("harness_lab_invariant_violation_fails");
5149 }
5150
5151 #[test]
5152 fn harness_receives_correct_seeds() {
5153 use std::sync::Arc;
5154 use std::sync::atomic::{AtomicU64, Ordering};
5155 init_test("harness_receives_correct_seeds");
5156
5157 let captured_lab_seed = Arc::new(AtomicU64::new(0));
5158 let captured_live_seed = Arc::new(AtomicU64::new(0));
5159 let lab_clone = Arc::clone(&captured_lab_seed);
5160 let live_clone = Arc::clone(&captured_live_seed);
5161
5162 let result = DualRunHarness::phase1("test.seeds", "s", "v1", "d", 0xBEEF)
5163 .lab(move |config| {
5164 lab_clone.store(config.seed, Ordering::Relaxed);
5165 make_happy_semantics()
5166 })
5167 .live(move |seed, _entropy| {
5168 live_clone.store(seed, Ordering::Relaxed);
5169 make_happy_semantics()
5170 })
5171 .run();
5172
5173 assert!(result.passed());
5174 assert_eq!(captured_lab_seed.load(Ordering::Relaxed), 0xBEEF);
5175 assert_eq!(captured_live_seed.load(Ordering::Relaxed), 0xBEEF);
5176 crate::test_complete!("harness_receives_correct_seeds");
5177 }
5178
5179 #[test]
5180 fn harness_with_custom_seed_plan() {
5181 use std::sync::Arc;
5182 use std::sync::atomic::{AtomicU64, Ordering};
5183 init_test("harness_with_custom_seed_plan");
5184
5185 let captured_lab = Arc::new(AtomicU64::new(0));
5186 let captured_live = Arc::new(AtomicU64::new(0));
5187 let lab_c = Arc::clone(&captured_lab);
5188 let live_c = Arc::clone(&captured_live);
5189
5190 let plan = SeedPlan::inherit(42, "custom")
5191 .with_lab_override(0xCAFE)
5192 .with_live_override(0xFACE);
5193
5194 let result = DualRunHarness::phase1("test", "s", "v1", "d", 42)
5195 .with_seed_plan(plan)
5196 .lab(move |config| {
5197 lab_c.store(config.seed, Ordering::Relaxed);
5198 make_happy_semantics()
5199 })
5200 .live(move |seed, _entropy| {
5201 live_c.store(seed, Ordering::Relaxed);
5202 make_happy_semantics()
5203 })
5204 .run();
5205
5206 assert_eq!(captured_lab.load(Ordering::Relaxed), 0xCAFE);
5207 assert_eq!(captured_live.load(Ordering::Relaxed), 0xFACE);
5208 assert!(result.verdict.is_equivalent());
5210 assert!(!result.seed_lineage.seeds_match);
5212 crate::test_complete!("harness_with_custom_seed_plan");
5213 }
5214
5215 #[test]
5216 fn harness_from_identity() {
5217 init_test("harness_from_identity");
5218 let ident = DualRunScenarioIdentity::phase1("test", "s", "v1", "d", 99);
5219 let result = DualRunHarness::from_identity(ident)
5220 .lab(|_| make_happy_semantics())
5221 .live(|_, _| make_happy_semantics())
5222 .run();
5223 assert!(result.passed());
5224 assert_eq!(result.verdict.scenario_id, "test");
5225 crate::test_complete!("harness_from_identity");
5226 }
5227
5228 #[test]
5229 fn dual_run_result_display() {
5230 init_test("dual_run_result_display");
5231 let result = DualRunHarness::phase1("test", "s", "v1", "d", 42)
5232 .lab(|_| make_happy_semantics())
5233 .live(|_, _| make_happy_semantics())
5234 .run();
5235 let summary = format!("{result}");
5236 assert!(summary.contains("PASS"));
5237 crate::test_complete!("dual_run_result_display");
5238 }
5239
5240 #[test]
5241 fn harness_noise_notes_classify_scheduler_noise() {
5242 init_test("harness_noise_notes_classify_scheduler_noise");
5243 let ident = DualRunScenarioIdentity::phase1("test.noise", "test.surface", "v1", "d", 42);
5244 let live_ident = ident.clone();
5245
5246 let result = DualRunHarness::from_identity(ident)
5247 .lab(|_| make_happy_semantics())
5248 .live_result(move |_, _| {
5249 let mut result = run_live_adapter(&live_ident, |_config, witness| {
5250 witness.set_outcome(TerminalOutcome::ok());
5251 witness.note_nondeterminism("thread scheduling");
5252 });
5253 result.semantics.resource_surface = ResourceSurfaceRecord::empty("test");
5254 result
5255 })
5256 .run();
5257
5258 assert!(result.passed());
5259 assert_eq!(
5260 result.policy.provisional_class,
5261 ProvisionalDivergenceClass::SchedulerNoiseSuspected
5262 );
5263 assert_eq!(
5264 result.policy.rerun_decision,
5265 RerunDecision::LiveConfirmations { additional_runs: 2 }
5266 );
5267 assert_eq!(
5268 result.policy.scheduler_noise_class,
5269 SchedulerNoiseClass::NondeterminismNotesOnly
5270 );
5271 crate::test_complete!("harness_noise_notes_classify_scheduler_noise");
5272 }
5273
5274 #[test]
5275 fn classify_scheduler_noise_prefers_hash_drift_over_notes() {
5276 init_test("classify_scheduler_noise_prefers_hash_drift_over_notes");
5277 let lab = make_observable(RuntimeKind::Lab, make_happy_semantics());
5278 let mut live = make_observable(RuntimeKind::Live, make_happy_semantics());
5279 let mut lab_prov = lab.provenance.clone();
5280 lab_prov.schedule_hash = Some(0xAAAA);
5281 let mut live_prov = live.provenance.clone();
5282 live_prov.schedule_hash = Some(0xBBBB);
5283 live_prov.nondeterminism_notes = vec!["thread scheduling".to_string()];
5284
5285 let lab = NormalizedObservable {
5286 provenance: lab_prov,
5287 ..lab
5288 };
5289 live.provenance = live_prov;
5290
5291 assert_eq!(
5292 classify_scheduler_noise(&lab, &live),
5293 SchedulerNoiseClass::ScheduleHashDrift
5294 );
5295 crate::test_complete!("classify_scheduler_noise_prefers_hash_drift_over_notes");
5296 }
5297
5298 #[test]
5299 fn harness_semantic_mismatch_policy_requests_reruns() {
5300 init_test("harness_semantic_mismatch_policy_requests_reruns");
5301 let result = DualRunHarness::phase1("test.mismatch.policy", "test.surface", "v1", "d", 42)
5302 .lab(|_| make_happy_semantics())
5303 .live(|_, _| {
5304 let mut sem = make_happy_semantics();
5305 sem.terminal_outcome = TerminalOutcome::err("network_error");
5306 sem
5307 })
5308 .run();
5309
5310 assert_eq!(
5311 result.policy.provisional_class,
5312 ProvisionalDivergenceClass::SemanticMismatchAdmittedSurface
5313 );
5314 assert_eq!(
5315 result.policy.rerun_decision,
5316 RerunDecision::DeterministicLabReplayAndLiveConfirmations {
5317 additional_live_runs: 2,
5318 }
5319 );
5320 assert_eq!(result.policy.suggested_final_class, None);
5321 crate::test_complete!("harness_semantic_mismatch_policy_requests_reruns");
5322 }
5323
5324 #[test]
5325 fn harness_unsupported_surface_policy_short_circuits() {
5326 init_test("harness_unsupported_surface_policy_short_circuits");
5327 let ident =
5328 DualRunScenarioIdentity::phase1("test.unsupported", "browser.surface", "v1", "d", 42)
5329 .with_metadata("eligibility_verdict", "unsupported")
5330 .with_metadata("unsupported_reason", "browser timing surface not admitted");
5331
5332 let result = DualRunHarness::from_identity(ident)
5333 .lab(|_| make_happy_semantics())
5334 .live(|_, _| {
5335 let mut sem = make_happy_semantics();
5336 sem.terminal_outcome = TerminalOutcome::err("unsupported_surface");
5337 sem
5338 })
5339 .run();
5340
5341 assert_eq!(
5342 result.policy.provisional_class,
5343 ProvisionalDivergenceClass::UnsupportedSurface
5344 );
5345 assert_eq!(result.policy.rerun_decision, RerunDecision::None);
5346 assert_eq!(
5347 result.policy.suggested_final_class,
5348 Some(FinalDivergenceClass::UnsupportedSurface)
5349 );
5350 crate::test_complete!("harness_unsupported_surface_policy_short_circuits");
5351 }
5352
5353 #[test]
5354 fn harness_insufficient_observability_policy_marks_gap() {
5355 init_test("harness_insufficient_observability_policy_marks_gap");
5356 let ident =
5357 DualRunScenarioIdentity::phase1("test.observability", "timer.surface", "v1", "d", 42)
5358 .with_metadata("observability_status", "blocked_missing_live_timer_surface");
5359
5360 let result = DualRunHarness::from_identity(ident)
5361 .lab(|_| {
5362 let mut sem = make_happy_semantics();
5363 sem.resource_surface =
5364 ResourceSurfaceRecord::empty("timer.surface").with_counter("timeouts", 1);
5365 sem
5366 })
5367 .live(|_, _| {
5368 let mut sem = make_happy_semantics();
5369 sem.resource_surface = ResourceSurfaceRecord::empty("timer.surface");
5370 sem
5371 })
5372 .run();
5373
5374 assert_eq!(
5375 result.policy.provisional_class,
5376 ProvisionalDivergenceClass::InsufficientObservability
5377 );
5378 assert_eq!(
5379 result.policy.rerun_decision,
5380 RerunDecision::ConfirmationIfRicherInstrumentationEnabled { additional_runs: 1 }
5381 );
5382 assert_eq!(
5383 result.policy.suggested_final_class,
5384 Some(FinalDivergenceClass::InsufficientObservability)
5385 );
5386 crate::test_complete!("harness_insufficient_observability_policy_marks_gap");
5387 }
5388
5389 #[test]
5390 fn harness_blocked_missing_observability_gate_is_not_a_pass() {
5391 init_test("harness_blocked_missing_observability_gate_is_not_a_pass");
5392 let ident = DualRunScenarioIdentity::phase1(
5393 "test.observability.gate",
5394 "timer.surface",
5395 "v1",
5396 "d",
5397 42,
5398 )
5399 .with_metadata("eligibility_verdict", "blocked_missing_observability");
5400
5401 let result = DualRunHarness::from_identity(ident)
5402 .lab(|_| make_happy_semantics())
5403 .live(|_, _| make_happy_semantics())
5404 .run();
5405
5406 assert_eq!(
5407 result.policy.provisional_class,
5408 ProvisionalDivergenceClass::InsufficientObservability
5409 );
5410 assert_eq!(
5411 result.policy.rerun_decision,
5412 RerunDecision::ConfirmationIfRicherInstrumentationEnabled { additional_runs: 1 }
5413 );
5414 assert_eq!(
5415 result.policy.suggested_final_class,
5416 Some(FinalDivergenceClass::InsufficientObservability)
5417 );
5418 crate::test_complete!("harness_blocked_missing_observability_gate_is_not_a_pass");
5419 }
5420
5421 #[test]
5422 fn harness_bridge_only_downgrade_can_still_be_an_admitted_surface() {
5423 init_test("harness_bridge_only_downgrade_can_still_be_an_admitted_surface");
5424 let ident = DualRunScenarioIdentity::phase1(
5425 "test.bridge_only_admitted",
5426 "browser.surface",
5427 "v1",
5428 "bridge-only downgrade remains comparable when admitted",
5429 42,
5430 )
5431 .with_metadata("eligibility_verdict", "eligible_for_pilot")
5432 .with_metadata("support_class", "bridge_only")
5433 .with_metadata("reason_code", "downgrade_to_server_bridge");
5434
5435 let result = DualRunHarness::from_identity(ident)
5436 .lab(|_| make_happy_semantics())
5437 .live(|_, _| make_happy_semantics())
5438 .run();
5439
5440 assert!(result.passed(), "{}", result.summary());
5441 assert_eq!(
5442 result.policy.provisional_class,
5443 ProvisionalDivergenceClass::Pass
5444 );
5445 assert_eq!(result.policy.rerun_decision, RerunDecision::None);
5446 assert_eq!(result.policy.suggested_final_class, None);
5447 crate::test_complete!("harness_bridge_only_downgrade_can_still_be_an_admitted_surface");
5448 }
5449
5450 #[test]
5451 fn harness_bridge_only_without_downgrade_reason_stays_unsupported() {
5452 init_test("harness_bridge_only_without_downgrade_reason_stays_unsupported");
5453 let ident = DualRunScenarioIdentity::phase1(
5454 "test.bridge_only_invalid_reason",
5455 "browser.surface",
5456 "v1",
5457 "bridge-only without a supported downgrade reason must fail closed",
5458 42,
5459 )
5460 .with_metadata("support_class", "bridge_only")
5461 .with_metadata("reason_code", "unsupported_runtime_context")
5462 .with_metadata(
5463 "unsupported_reason",
5464 "non-browser runtime context has no admitted downgrade lane",
5465 );
5466
5467 let result = DualRunHarness::from_identity(ident)
5468 .lab(|_| make_happy_semantics())
5469 .live(|_, _| make_happy_semantics())
5470 .run();
5471
5472 assert_eq!(
5473 result.policy.provisional_class,
5474 ProvisionalDivergenceClass::UnsupportedSurface
5475 );
5476 assert_eq!(result.policy.rerun_decision, RerunDecision::None);
5477 assert_eq!(
5478 result.policy.suggested_final_class,
5479 Some(FinalDivergenceClass::UnsupportedSurface)
5480 );
5481 crate::test_complete!("harness_bridge_only_without_downgrade_reason_stays_unsupported");
5482 }
5483
5484 #[test]
5485 fn harness_eligible_gate_does_not_override_unsupported_support_class() {
5486 init_test("harness_eligible_gate_does_not_override_unsupported_support_class");
5487 let ident = DualRunScenarioIdentity::phase1(
5488 "test.eligible_gate_conflict",
5489 "browser.surface",
5490 "v1",
5491 "contradictory unsupported support class must fail closed",
5492 42,
5493 )
5494 .with_metadata("eligibility_verdict", "eligible_for_pilot")
5495 .with_metadata("support_class", "unsupported")
5496 .with_metadata(
5497 "unsupported_reason",
5498 "shared worker direct runtime not shipped",
5499 );
5500
5501 let result = DualRunHarness::from_identity(ident)
5502 .lab(|_| make_happy_semantics())
5503 .live(|_, _| make_happy_semantics())
5504 .run();
5505
5506 assert_eq!(
5507 result.policy.provisional_class,
5508 ProvisionalDivergenceClass::UnsupportedSurface
5509 );
5510 assert_eq!(result.policy.rerun_decision, RerunDecision::None);
5511 assert_eq!(
5512 result.policy.suggested_final_class,
5513 Some(FinalDivergenceClass::UnsupportedSurface)
5514 );
5515 crate::test_complete!("harness_eligible_gate_does_not_override_unsupported_support_class");
5516 }
5517
5518 #[test]
5519 fn harness_blocked_missing_verification_gate_stays_unsupported() {
5520 init_test("harness_blocked_missing_verification_gate_stays_unsupported");
5521 let ident = DualRunScenarioIdentity::phase1(
5522 "test.verification.gate",
5523 "browser.surface",
5524 "v1",
5525 "d",
5526 42,
5527 )
5528 .with_metadata("eligibility_verdict", "blocked_missing_verification")
5529 .with_metadata("support_class", "bridge_only")
5530 .with_metadata("reason_code", "downgrade_to_server_bridge");
5531
5532 let result = DualRunHarness::from_identity(ident)
5533 .lab(|_| make_happy_semantics())
5534 .live(|_, _| make_happy_semantics())
5535 .run();
5536
5537 assert_eq!(
5538 result.policy.provisional_class,
5539 ProvisionalDivergenceClass::UnsupportedSurface
5540 );
5541 assert_eq!(result.policy.rerun_decision, RerunDecision::None);
5542 assert_eq!(
5543 result.policy.suggested_final_class,
5544 Some(FinalDivergenceClass::UnsupportedSurface)
5545 );
5546 crate::test_complete!("harness_blocked_missing_verification_gate_stays_unsupported");
5547 }
5548
5549 #[test]
5550 fn harness_hard_contract_break_policy_short_circuits() {
5551 init_test("harness_hard_contract_break_policy_short_circuits");
5552 let result = DualRunHarness::phase1("test.hard_break", "test.surface", "v1", "d", 42)
5553 .lab(|_| make_happy_semantics())
5554 .live(|_, _| {
5555 let mut sem = make_happy_semantics();
5556 sem.obligation_balance.leaked = 1;
5557 sem.obligation_balance.balanced = false;
5558 sem
5559 })
5560 .run();
5561
5562 assert_eq!(
5563 result.policy.provisional_class,
5564 ProvisionalDivergenceClass::HardContractBreak
5565 );
5566 assert_eq!(result.policy.rerun_decision, RerunDecision::None);
5567 assert_eq!(
5568 result.policy.suggested_final_class,
5569 Some(FinalDivergenceClass::RuntimeSemanticBug)
5570 );
5571 crate::test_complete!("harness_hard_contract_break_policy_short_circuits");
5572 }
5573
5574 #[test]
5575 #[should_panic(expected = "Dual-run test failed")]
5576 fn assert_dual_run_passes_panics_on_failure() {
5577 init_test("assert_dual_run_passes_panics_on_failure");
5578 let result = DualRunHarness::phase1("test", "s", "v1", "d", 42)
5579 .lab(|_| make_happy_semantics())
5580 .live(|_, _| {
5581 let mut sem = make_happy_semantics();
5582 sem.terminal_outcome = TerminalOutcome::err("oops");
5583 sem
5584 })
5585 .run();
5586 assert_dual_run_passes(&result);
5587 }
5588
5589 #[test]
5592 fn live_runner_config_from_identity() {
5593 init_test("live_runner_config_from_identity");
5594 let ident = DualRunScenarioIdentity::phase1("test", "surface", "v1", "d", 0xBEEF);
5595 let config = LiveRunnerConfig::from_identity(&ident);
5596 assert_eq!(config.seed, 0xBEEF);
5597 assert_eq!(config.profile, LiveExecutionProfile::CurrentThread);
5598 assert_eq!(config.scenario_id, "test");
5599 assert_eq!(config.surface_id, "surface");
5600 crate::test_complete!("live_runner_config_from_identity");
5601 }
5602
5603 #[test]
5604 fn live_runner_config_from_plan() {
5605 init_test("live_runner_config_from_plan");
5606 let plan = SeedPlan::inherit(42, "lineage").with_live_override(0xCAFE);
5607 let config = LiveRunnerConfig::from_plan(&plan, "scenario", "surface");
5608 assert_eq!(config.seed, 0xCAFE);
5609 assert_eq!(config.seed_lineage_id, "lineage");
5610 crate::test_complete!("live_runner_config_from_plan");
5611 }
5612
5613 #[test]
5614 fn live_runner_config_display() {
5615 init_test("live_runner_config_display");
5616 let ident = DualRunScenarioIdentity::phase1("test", "s", "v1", "d", 42);
5617 let config = LiveRunnerConfig::from_identity(&ident);
5618 let s = format!("{config}");
5619 assert!(s.contains("test"));
5620 assert!(s.contains("current_thread"));
5621 crate::test_complete!("live_runner_config_display");
5622 }
5623
5624 #[test]
5625 fn live_witness_collector_defaults() {
5626 init_test("live_witness_collector_defaults");
5627 let witness = LiveWitnessCollector::new("test.surface");
5628 let sem = witness.finalize();
5629 assert_eq!(sem.terminal_outcome.class, OutcomeClass::Ok);
5630 assert!(sem.region_close.quiescent);
5631 assert!(sem.obligation_balance.balanced);
5632 assert_eq!(sem.loser_drain.status, DrainStatus::NotApplicable);
5633 assert_eq!(sem.resource_surface.contract_scope, "test.surface");
5634 crate::test_complete!("live_witness_collector_defaults");
5635 }
5636
5637 #[test]
5638 fn live_witness_collector_records_evidence() {
5639 init_test("live_witness_collector_records_evidence");
5640 let mut witness = LiveWitnessCollector::new("test");
5641 witness.set_outcome(TerminalOutcome::cancelled("timeout"));
5642 witness.set_cancellation(CancellationRecord::completed());
5643 witness.set_loser_drain(LoserDrainRecord::complete(2));
5644 witness.set_obligation_balance(ObligationBalanceRecord::balanced(5, 4, 1));
5645 witness.record_counter("msgs_sent", 10);
5646 witness.record_counter_with_tolerance("bytes", 1024, CounterTolerance::AtLeast);
5647 witness.note_nondeterminism("scheduler ordering may vary");
5648
5649 assert_eq!(witness.nondeterminism_notes().len(), 1);
5650
5651 let sem = witness.finalize();
5652 assert_eq!(sem.terminal_outcome.class, OutcomeClass::Cancelled);
5653 assert!(sem.cancellation.requested);
5654 assert_eq!(sem.loser_drain.drained_losers, 2);
5655 assert_eq!(sem.obligation_balance.committed, 4);
5656 assert_eq!(sem.resource_surface.counters["msgs_sent"], 10);
5657 assert_eq!(
5658 sem.resource_surface.tolerances["bytes"],
5659 CounterTolerance::AtLeast
5660 );
5661 crate::test_complete!("live_witness_collector_records_evidence");
5662 }
5663
5664 #[test]
5665 fn run_live_adapter_happy_path() {
5666 init_test("run_live_adapter_happy_path");
5667 let ident = DualRunScenarioIdentity::phase1(
5668 "test.happy",
5669 "test.surface",
5670 "v1",
5671 "Happy path live adapter test",
5672 42,
5673 );
5674 let result = run_live_adapter(&ident, |config, witness| {
5675 assert_eq!(config.seed, 42);
5676 assert_eq!(config.profile, LiveExecutionProfile::CurrentThread);
5677 witness.set_outcome(TerminalOutcome::ok());
5678 witness.record_counter("items_processed", 5);
5679 });
5680 assert_eq!(result.semantics.terminal_outcome.class, OutcomeClass::Ok);
5681 assert_eq!(
5682 result.semantics.resource_surface.counters["items_processed"],
5683 5
5684 );
5685 assert_eq!(result.metadata.config.scenario_id, "test.happy");
5686 assert!(result.metadata.nondeterminism_notes.is_empty());
5687 assert_eq!(
5688 result
5689 .metadata
5690 .capture_manifest
5691 .describe_field_capture("semantics.terminal_outcome.class")
5692 .as_deref(),
5693 Some("observed via witness.set_outcome")
5694 );
5695 assert_eq!(
5696 result
5697 .metadata
5698 .capture_manifest
5699 .describe_field_capture("semantics.resource_surface.counters.items_processed")
5700 .as_deref(),
5701 Some("observed via witness.record_counter")
5702 );
5703 crate::test_complete!("run_live_adapter_happy_path");
5704 }
5705
5706 #[test]
5707 fn run_live_adapter_with_nondeterminism() {
5708 init_test("run_live_adapter_with_nondeterminism");
5709 let ident = DualRunScenarioIdentity::phase1("test", "s", "v1", "d", 42);
5710 let result = run_live_adapter(&ident, |_config, witness| {
5711 witness.note_nondeterminism("timer resolution varies");
5712 witness.note_nondeterminism("thread scheduling");
5713 });
5714 assert_eq!(result.metadata.nondeterminism_notes.len(), 2);
5715 assert_eq!(
5716 result.metadata.replay.nondeterminism_notes,
5717 result.metadata.nondeterminism_notes
5718 );
5719 crate::test_complete!("run_live_adapter_with_nondeterminism");
5720 }
5721
5722 #[test]
5723 fn run_live_adapter_cancellation_scenario() {
5724 init_test("run_live_adapter_cancellation_scenario");
5725 let ident = DualRunScenarioIdentity::phase1(
5726 "cancel.race",
5727 "cancellation.race",
5728 "v1",
5729 "Cancel and drain",
5730 0xDEAD,
5731 );
5732 let result = run_live_adapter(&ident, |config, witness| {
5733 assert_eq!(config.seed, 0xDEAD);
5734 witness.set_outcome(TerminalOutcome::ok());
5735 witness.set_cancellation(CancellationRecord::completed());
5736 witness.set_loser_drain(LoserDrainRecord::complete(1));
5737 });
5738 assert!(result.semantics.cancellation.requested);
5739 assert!(result.semantics.cancellation.cleanup_completed);
5740 assert_eq!(result.semantics.loser_drain.status, DrainStatus::Complete);
5741 assert_eq!(
5742 result.metadata.replay.instance.runtime_kind,
5743 RuntimeKind::Live
5744 );
5745 assert_eq!(
5746 result
5747 .metadata
5748 .capture_manifest
5749 .describe_field_capture("semantics.cancellation.checkpoint_observed")
5750 .as_deref(),
5751 Some("observed via witness.set_cancellation")
5752 );
5753 crate::test_complete!("run_live_adapter_cancellation_scenario");
5754 }
5755
5756 #[test]
5757 fn run_live_adapter_metadata_serde() {
5758 init_test("run_live_adapter_metadata_serde");
5759 let ident = DualRunScenarioIdentity::phase1("test", "s", "v1", "d", 42);
5760 let result = run_live_adapter(&ident, |_, _| {});
5761 let json = serde_json::to_string_pretty(&result.metadata).unwrap();
5762 let parsed: LiveRunMetadata = serde_json::from_str(&json).unwrap();
5763 assert_eq!(parsed.config.seed, 42);
5764 assert_eq!(parsed.config.profile, LiveExecutionProfile::CurrentThread);
5765 assert_eq!(
5766 parsed
5767 .capture_manifest
5768 .describe_field_capture("semantics.region_close.quiescent")
5769 .as_deref(),
5770 Some("inferred via run_live_adapter.default_quiescent")
5771 );
5772 crate::test_complete!("run_live_adapter_metadata_serde");
5773 }
5774
5775 #[test]
5776 fn live_adapter_integrates_with_harness() {
5777 init_test("live_adapter_integrates_with_harness");
5778 let result = DualRunHarness::phase1(
5781 "integration.test",
5782 "test.surface",
5783 "v1",
5784 "Full integration of live adapter with harness",
5785 0xBEEF,
5786 )
5787 .lab(|_config| make_happy_semantics())
5788 .live(|seed, _entropy| {
5789 let ident = DualRunScenarioIdentity::phase1(
5790 "integration.test",
5791 "test.surface",
5792 "v1",
5793 "d",
5794 seed,
5795 );
5796 let live_result = run_live_adapter(&ident, |_config, witness| {
5797 witness.set_outcome(TerminalOutcome::ok());
5798 witness.record_counter("items", 3);
5799 });
5800 live_result.semantics
5801 })
5802 .run();
5803
5804 assert!(!result.verdict.passed); crate::test_complete!("live_adapter_integrates_with_harness");
5809 }
5810
5811 #[test]
5814 fn capture_manifest_tracking() {
5815 init_test("capture_manifest_tracking");
5816 let mut manifest = CaptureManifest::new();
5817 manifest.observed("terminal_outcome", "outcome_match");
5818 manifest.inferred("cancellation.acknowledged", "task_handle.join");
5819 manifest.unsupported("cancellation.checkpoint_observed");
5820
5821 assert_eq!(manifest.total_fields(), 3);
5822 assert_eq!(manifest.unsupported_count(), 1);
5823 assert!(!manifest.fully_observed());
5824 assert_eq!(
5825 manifest.unsupported_fields,
5826 vec!["cancellation.checkpoint_observed"]
5827 );
5828 crate::test_complete!("capture_manifest_tracking");
5829 }
5830
5831 #[test]
5832 fn capture_manifest_fully_observed() {
5833 init_test("capture_manifest_fully_observed");
5834 let mut manifest = CaptureManifest::new();
5835 manifest.observed("outcome", "match");
5836 manifest.observed("cancel", "hook");
5837 assert!(manifest.fully_observed());
5838 crate::test_complete!("capture_manifest_fully_observed");
5839 }
5840
5841 #[test]
5842 fn capture_manifest_empty_is_not_fully_observed() {
5843 init_test("capture_manifest_empty_is_not_fully_observed");
5844 let manifest = CaptureManifest::new();
5845 assert!(!manifest.fully_observed());
5846 crate::test_complete!("capture_manifest_empty_is_not_fully_observed");
5847 }
5848
5849 #[test]
5850 fn capture_manifest_serde() {
5851 init_test("capture_manifest_serde");
5852 let mut manifest = CaptureManifest::new();
5853 manifest.observed("outcome", "match");
5854 manifest.unsupported("checkpoint");
5855 let json = serde_json::to_string(&manifest).unwrap();
5856 let parsed: CaptureManifest = serde_json::from_str(&json).unwrap();
5857 assert_eq!(parsed.total_fields(), 2);
5858 crate::test_complete!("capture_manifest_serde");
5859 }
5860
5861 #[test]
5862 fn capture_manifest_canonicalizes_and_resolves_parent_fields() {
5863 init_test("capture_manifest_canonicalizes_and_resolves_parent_fields");
5864 let mut manifest = CaptureManifest::new();
5865 manifest.unsupported("terminal_outcome");
5866 manifest.observed("resource_surface.counters.items", "counter");
5867 manifest.observed("terminal_outcome", "hook");
5868 manifest.inferred("cancellation", "fallback");
5869
5870 let fields: Vec<&str> = manifest
5871 .annotations
5872 .iter()
5873 .map(|annotation| annotation.field.as_str())
5874 .collect();
5875 assert_eq!(
5876 fields,
5877 vec![
5878 "cancellation",
5879 "resource_surface.counters.items",
5880 "terminal_outcome"
5881 ]
5882 );
5883 assert!(manifest.unsupported_fields.is_empty());
5884 assert_eq!(
5885 manifest
5886 .annotation_for_field("semantics.resource_surface.counters.items")
5887 .unwrap()
5888 .source,
5889 "counter"
5890 );
5891 assert_eq!(
5892 manifest
5893 .describe_field_capture("semantics.terminal_outcome.class")
5894 .as_deref(),
5895 Some("observed via hook")
5896 );
5897 crate::test_complete!("capture_manifest_canonicalizes_and_resolves_parent_fields");
5898 }
5899
5900 #[test]
5901 fn capture_terminal_from_outcome_ok() {
5902 init_test("capture_terminal_from_outcome_ok");
5903 let outcome: crate::types::outcome::Outcome<i32, String> =
5904 crate::types::outcome::Outcome::Ok(42);
5905 let t = capture_terminal_outcome(&outcome);
5906 assert_eq!(t.class, OutcomeClass::Ok);
5907 assert_eq!(t.severity, OutcomeClass::Ok);
5908 crate::test_complete!("capture_terminal_from_outcome_ok");
5909 }
5910
5911 #[test]
5912 fn capture_terminal_from_outcome_err() {
5913 init_test("capture_terminal_from_outcome_err");
5914 let outcome: crate::types::outcome::Outcome<i32, String> =
5915 crate::types::outcome::Outcome::Err("network_error".to_string());
5916 let t = capture_terminal_outcome(&outcome);
5917 assert_eq!(t.class, OutcomeClass::Err);
5918 assert_eq!(t.error_class.as_deref(), Some("network_error"));
5919 crate::test_complete!("capture_terminal_from_outcome_err");
5920 }
5921
5922 #[test]
5923 fn capture_terminal_from_outcome_cancelled() {
5924 init_test("capture_terminal_from_outcome_cancelled");
5925 let outcome: crate::types::outcome::Outcome<i32, String> =
5926 crate::types::outcome::Outcome::Cancelled(crate::types::CancelReason::new(
5927 crate::types::CancelKind::User,
5928 ));
5929 let t = capture_terminal_outcome(&outcome);
5930 assert_eq!(t.class, OutcomeClass::Cancelled);
5931 assert!(t.cancel_reason_class.is_some());
5932 crate::test_complete!("capture_terminal_from_outcome_cancelled");
5933 }
5934
5935 #[test]
5936 fn capture_terminal_from_result_ok_and_err() {
5937 init_test("capture_terminal_from_result_ok_and_err");
5938 let ok: Result<i32, String> = Ok(42);
5939 let err: Result<i32, String> = Err("fail".to_string());
5940 assert_eq!(
5941 super::capture_terminal_from_result(&ok).class,
5942 OutcomeClass::Ok
5943 );
5944 assert_eq!(
5945 super::capture_terminal_from_result(&err).class,
5946 OutcomeClass::Err
5947 );
5948 crate::test_complete!("capture_terminal_from_result_ok_and_err");
5949 }
5950
5951 #[test]
5952 fn capture_obligation_balanced() {
5953 init_test("capture_obligation_balanced");
5954 let b = capture_obligation_balance(10, 8, 2);
5955 assert!(b.balanced);
5956 assert_eq!(b.leaked, 0);
5957 assert_eq!(b.unresolved, 0);
5958 crate::test_complete!("capture_obligation_balanced");
5959 }
5960
5961 #[test]
5962 fn capture_obligation_leaked() {
5963 init_test("capture_obligation_leaked");
5964 let b = capture_obligation_balance(10, 5, 2);
5965 assert!(!b.balanced);
5966 assert_eq!(b.leaked, 3);
5967 crate::test_complete!("capture_obligation_leaked");
5968 }
5969
5970 #[test]
5971 fn capture_region_close_quiescent() {
5972 init_test("capture_region_close_quiescent");
5973 let r = capture_region_close(true, true);
5974 assert!(r.quiescent);
5975 assert!(r.close_completed);
5976 assert_eq!(r.root_state, RegionState::Closed);
5977 assert_eq!(r.live_children, 0);
5978 crate::test_complete!("capture_region_close_quiescent");
5979 }
5980
5981 #[test]
5982 fn capture_region_close_not_quiescent() {
5983 init_test("capture_region_close_not_quiescent");
5984 let r = capture_region_close(false, true);
5985 assert!(!r.quiescent);
5986 assert!(!r.close_completed);
5987 assert_eq!(r.root_state, RegionState::Draining);
5988 assert_eq!(r.live_children, 1);
5989 assert_eq!(r.finalizers_pending, 0);
5990 crate::test_complete!("capture_region_close_not_quiescent");
5991 }
5992
5993 #[test]
5994 fn capture_region_close_finalizing() {
5995 init_test("capture_region_close_finalizing");
5996 let r = capture_region_close(true, false);
5997 assert!(!r.quiescent);
5998 assert!(!r.close_completed);
5999 assert_eq!(r.root_state, RegionState::Finalizing);
6000 assert_eq!(r.live_children, 0);
6001 assert_eq!(r.finalizers_pending, 1);
6002 crate::test_complete!("capture_region_close_finalizing");
6003 }
6004
6005 #[test]
6006 fn capture_loser_drain_not_applicable() {
6007 init_test("capture_loser_drain_not_applicable");
6008 let d = capture_loser_drain(&[]);
6009 assert!(!d.applicable);
6010 assert_eq!(d.status, DrainStatus::NotApplicable);
6011 crate::test_complete!("capture_loser_drain_not_applicable");
6012 }
6013
6014 #[test]
6015 fn capture_loser_drain_all_drained() {
6016 init_test("capture_loser_drain_all_drained");
6017 let d = capture_loser_drain(&[true, true, true]);
6018 assert!(d.applicable);
6019 assert_eq!(d.status, DrainStatus::Complete);
6020 assert_eq!(d.expected_losers, 3);
6021 assert_eq!(d.drained_losers, 3);
6022 crate::test_complete!("capture_loser_drain_all_drained");
6023 }
6024
6025 #[test]
6026 fn capture_loser_drain_partial() {
6027 init_test("capture_loser_drain_partial");
6028 let d = capture_loser_drain(&[true, false, true]);
6029 assert_eq!(d.status, DrainStatus::Incomplete);
6030 assert_eq!(d.drained_losers, 2);
6031 crate::test_complete!("capture_loser_drain_partial");
6032 }
6033
6034 #[test]
6035 fn capture_cancellation_not_cancelled() {
6036 init_test("capture_cancellation_not_cancelled");
6037 let c = capture_cancellation(false, false, false, false, None);
6038 assert_eq!(c.terminal_phase, CancelTerminalPhase::NotCancelled);
6039 assert!(!c.requested);
6040 crate::test_complete!("capture_cancellation_not_cancelled");
6041 }
6042
6043 #[test]
6044 fn capture_cancellation_completed() {
6045 init_test("capture_cancellation_completed");
6046 let c = capture_cancellation(true, true, true, true, Some(true));
6047 assert_eq!(c.terminal_phase, CancelTerminalPhase::Completed);
6048 assert!(c.requested);
6049 assert!(c.acknowledged);
6050 assert!(c.cleanup_completed);
6051 assert!(c.finalization_completed);
6052 assert_eq!(c.checkpoint_observed, Some(true));
6053 crate::test_complete!("capture_cancellation_completed");
6054 }
6055
6056 #[test]
6057 fn capture_cancellation_in_progress() {
6058 init_test("capture_cancellation_in_progress");
6059 let c = capture_cancellation(true, true, false, false, None);
6060 assert_eq!(c.terminal_phase, CancelTerminalPhase::Cancelling);
6061 crate::test_complete!("capture_cancellation_in_progress");
6062 }
6063
6064 #[test]
6065 fn capture_cancellation_finalizing() {
6066 init_test("capture_cancellation_finalizing");
6067 let c = capture_cancellation(true, true, true, false, None);
6068 assert_eq!(c.terminal_phase, CancelTerminalPhase::Finalizing);
6069 crate::test_complete!("capture_cancellation_finalizing");
6070 }
6071
6072 fn make_passing_oracle_report() -> crate::lab::oracle::OracleReport {
6075 crate::lab::oracle::OracleReport {
6076 entries: vec![],
6077 total: 0,
6078 passed: 0,
6079 failed: 0,
6080 check_time_nanos: 0,
6081 }
6082 }
6083
6084 fn make_passing_lab_report(seed: u64) -> crate::lab::runtime::LabRunReport {
6085 crate::lab::runtime::LabRunReport {
6086 seed,
6087 steps_delta: 100,
6088 steps_total: 100,
6089 quiescent: true,
6090 now_nanos: 0,
6091 trace_len: 10,
6092 trace_fingerprint: 0xABCD,
6093 trace_certificate: crate::lab::runtime::LabTraceCertificateSummary {
6094 event_hash: 0x1234,
6095 event_count: 10,
6096 schedule_hash: 0x5678,
6097 },
6098 oracle_report: make_passing_oracle_report(),
6099 invariant_violations: vec![],
6100 temporal_invariant_failures: vec![],
6101 temporal_counterexample_prefix_len: None,
6102 refinement_firewall_rule_id: None,
6103 refinement_firewall_event_index: None,
6104 refinement_firewall_event_seq: None,
6105 refinement_counterexample_prefix_len: None,
6106 refinement_firewall_skipped_due_to_trace_truncation: false,
6107 }
6108 }
6109
6110 fn make_golden_live_result(identity: &DualRunScenarioIdentity) -> LiveRunResult {
6111 run_live_adapter(identity, |_, witness| {
6112 witness.set_outcome(TerminalOutcome::ok());
6113 witness.set_loser_drain(LoserDrainRecord::complete(2));
6114 witness.record_counter("items", 5);
6115 witness.record_counter_with_tolerance("bytes", 128, CounterTolerance::AtLeast);
6116 witness.note_nondeterminism("scheduler jitter");
6117 })
6118 }
6119
6120 #[test]
6121 fn normalize_lab_report_happy_path() {
6122 init_test("normalize_lab_report_happy_path");
6123 let report = make_passing_lab_report(42);
6124 let (sem, manifest) = normalize_lab_report(&report, "test.surface");
6125 assert_eq!(sem.terminal_outcome.class, OutcomeClass::Ok);
6126 assert!(sem.region_close.quiescent);
6127 assert!(sem.obligation_balance.balanced);
6128 assert!(manifest.total_fields() > 0);
6129 crate::test_complete!("normalize_lab_report_happy_path");
6130 }
6131
6132 #[test]
6133 #[allow(clippy::too_many_lines)]
6134 fn normalize_lab_report_matches_golden_record() {
6135 init_test("normalize_lab_report_matches_golden_record");
6136 let identity = DualRunScenarioIdentity::phase1(
6137 "golden.lab",
6138 "test.surface",
6139 "v1",
6140 "Golden lab normalization",
6141 42,
6142 );
6143 let report = make_passing_lab_report(42);
6144 let (semantics, manifest) = normalize_lab_report(&report, "test.surface");
6145 let observable = normalize_lab_observable(&identity, &report);
6146
6147 assert_eq!(observable.semantics, semantics);
6148 assert_eq!(
6149 serde_json::to_value(&manifest).unwrap(),
6150 serde_json::json!({
6151 "annotations": [
6152 {
6153 "field": "cancellation",
6154 "observability": "inferred",
6155 "source": "no_oracle_entry",
6156 },
6157 {
6158 "field": "loser_drain",
6159 "observability": "inferred",
6160 "source": "no_oracle_entry",
6161 },
6162 {
6163 "field": "obligation_balance",
6164 "observability": "observed",
6165 "source": "oracle.obligation_leak + invariants",
6166 },
6167 {
6168 "field": "region_close.quiescent",
6169 "observability": "observed",
6170 "source": "LabRunReport.quiescent",
6171 },
6172 {
6173 "field": "terminal_outcome",
6174 "observability": "observed",
6175 "source": "oracle_report.all_passed",
6176 }
6177 ],
6178 "unsupported_fields": [],
6179 })
6180 );
6181 assert_eq!(
6182 serde_json::to_value(&observable).unwrap(),
6183 serde_json::json!({
6184 "schema_version": NORMALIZED_OBSERVABLE_SCHEMA_VERSION,
6185 "scenario_id": "golden.lab",
6186 "surface_id": "test.surface",
6187 "surface_contract_version": "v1",
6188 "runtime_kind": "lab",
6189 "semantics": {
6190 "terminal_outcome": {
6191 "class": "ok",
6192 "severity": "ok",
6193 },
6194 "cancellation": {
6195 "requested": false,
6196 "acknowledged": false,
6197 "cleanup_completed": false,
6198 "finalization_completed": false,
6199 "terminal_phase": "not_cancelled",
6200 },
6201 "loser_drain": {
6202 "applicable": false,
6203 "expected_losers": 0,
6204 "drained_losers": 0,
6205 "status": "not_applicable",
6206 },
6207 "region_close": {
6208 "root_state": "closed",
6209 "quiescent": true,
6210 "live_children": 0,
6211 "finalizers_pending": 0,
6212 "close_completed": true,
6213 },
6214 "obligation_balance": {
6215 "reserved": 0,
6216 "committed": 0,
6217 "aborted": 0,
6218 "leaked": 0,
6219 "unresolved": 0,
6220 "balanced": true,
6221 },
6222 "resource_surface": {
6223 "contract_scope": "test.surface",
6224 "counters": {},
6225 "tolerances": {},
6226 },
6227 },
6228 "provenance": {
6229 "family": {
6230 "id": "golden.lab",
6231 "surface_id": "test.surface",
6232 "surface_contract_version": "v1",
6233 },
6234 "instance": {
6235 "family_id": "golden.lab",
6236 "effective_seed": 42,
6237 "runtime_kind": "lab",
6238 "run_index": 0,
6239 },
6240 "seed_plan": {
6241 "canonical_seed": 42,
6242 "seed_lineage_id": "golden.lab",
6243 "lab_seed_mode": "inherit",
6244 "live_seed_mode": "inherit",
6245 "replay_policy": "single_seed",
6246 },
6247 "effective_seed": 42,
6248 "effective_entropy_seed": derive_component_seed(42, "entropy"),
6249 "trace_fingerprint": 43981,
6250 "schedule_hash": 22136,
6251 "event_hash": 4660,
6252 "event_count": 10,
6253 "steps_total": 100,
6254 },
6255 })
6256 );
6257 crate::test_complete!("normalize_lab_report_matches_golden_record");
6258 }
6259
6260 #[test]
6261 fn normalize_lab_report_invariant_violation() {
6262 init_test("normalize_lab_report_invariant_violation");
6263 let mut report = make_passing_lab_report(42);
6264 report.invariant_violations = vec!["obligation leak detected".to_string()];
6265 let (sem, _) = normalize_lab_report(&report, "test");
6266 assert_eq!(sem.terminal_outcome.class, OutcomeClass::Err);
6267 assert!(!sem.obligation_balance.balanced);
6268 crate::test_complete!("normalize_lab_report_invariant_violation");
6269 }
6270
6271 #[test]
6272 fn normalize_lab_report_not_quiescent() {
6273 init_test("normalize_lab_report_not_quiescent");
6274 let mut report = make_passing_lab_report(42);
6275 report.quiescent = false;
6276 let (sem, _) = normalize_lab_report(&report, "test");
6277 assert!(!sem.region_close.quiescent);
6278 assert!(!sem.region_close.close_completed);
6279 assert_eq!(sem.region_close.root_state, RegionState::Closing);
6280 crate::test_complete!("normalize_lab_report_not_quiescent");
6281 }
6282
6283 #[test]
6284 fn normalize_lab_observable_preserves_provenance() {
6285 init_test("normalize_lab_observable_preserves_provenance");
6286 let ident = DualRunScenarioIdentity::phase1("test", "s", "v1", "d", 42);
6287 let report = make_passing_lab_report(42);
6288 let obs = normalize_lab_observable(&ident, &report);
6289 assert_eq!(obs.runtime_kind, RuntimeKind::Lab);
6290 assert_eq!(obs.provenance.trace_fingerprint, Some(0xABCD));
6291 assert_eq!(obs.provenance.event_hash, Some(0x1234));
6292 assert_eq!(obs.provenance.steps_total, Some(100));
6293 crate::test_complete!("normalize_lab_observable_preserves_provenance");
6294 }
6295
6296 #[test]
6297 fn normalize_live_observable_from_result() {
6298 init_test("normalize_live_observable_from_result");
6299 let ident = DualRunScenarioIdentity::phase1("test", "s", "v1", "d", 42);
6300 let live_result = run_live_adapter(&ident, |_, witness| {
6301 witness.set_outcome(TerminalOutcome::ok());
6302 witness.record_counter("items", 5);
6303 witness.note_nondeterminism("thread scheduling");
6304 });
6305 let obs = normalize_live_observable(&ident, &live_result);
6306 assert_eq!(obs.runtime_kind, RuntimeKind::Live);
6307 assert_eq!(obs.semantics.terminal_outcome.class, OutcomeClass::Ok);
6308 assert_eq!(obs.semantics.resource_surface.counters["items"], 5);
6309 assert_eq!(obs.provenance.nondeterminism_notes, ["thread scheduling"]);
6310 crate::test_complete!("normalize_live_observable_from_result");
6311 }
6312
6313 #[test]
6314 #[allow(clippy::too_many_lines)]
6315 fn normalize_live_observable_matches_golden_record_and_manifest() {
6316 init_test("normalize_live_observable_matches_golden_record_and_manifest");
6317 let identity = DualRunScenarioIdentity::phase1(
6318 "golden.live",
6319 "test.surface",
6320 "v1",
6321 "Golden live normalization",
6322 42,
6323 );
6324 let live_result = make_golden_live_result(&identity);
6325 let observable = normalize_live_observable(&identity, &live_result);
6326
6327 assert_eq!(
6328 serde_json::to_value(&live_result.metadata.capture_manifest).unwrap(),
6329 serde_json::json!({
6330 "annotations": [
6331 {
6332 "field": "cancellation",
6333 "observability": "inferred",
6334 "source": "run_live_adapter.default_no_cancellation",
6335 },
6336 {
6337 "field": "cancellation.checkpoint_observed",
6338 "observability": "unsupported",
6339 "source": "default",
6340 },
6341 {
6342 "field": "loser_drain",
6343 "observability": "observed",
6344 "source": "witness.set_loser_drain",
6345 },
6346 {
6347 "field": "obligation_balance",
6348 "observability": "inferred",
6349 "source": "run_live_adapter.default_balanced_obligations",
6350 },
6351 {
6352 "field": "region_close",
6353 "observability": "inferred",
6354 "source": "run_live_adapter.default_quiescent",
6355 },
6356 {
6357 "field": "resource_surface.contract_scope",
6358 "observability": "observed",
6359 "source": "scenario_identity.surface_id",
6360 },
6361 {
6362 "field": "resource_surface.counters.bytes",
6363 "observability": "observed",
6364 "source": "witness.record_counter_with_tolerance",
6365 },
6366 {
6367 "field": "resource_surface.counters.items",
6368 "observability": "observed",
6369 "source": "witness.record_counter",
6370 },
6371 {
6372 "field": "resource_surface.tolerances.bytes",
6373 "observability": "observed",
6374 "source": "witness.record_counter_with_tolerance",
6375 },
6376 {
6377 "field": "resource_surface.tolerances.items",
6378 "observability": "observed",
6379 "source": "witness.record_counter",
6380 },
6381 {
6382 "field": "terminal_outcome",
6383 "observability": "observed",
6384 "source": "witness.set_outcome",
6385 }
6386 ],
6387 "unsupported_fields": ["cancellation.checkpoint_observed"],
6388 })
6389 );
6390 assert_eq!(
6391 serde_json::to_value(&observable).unwrap(),
6392 serde_json::json!({
6393 "schema_version": NORMALIZED_OBSERVABLE_SCHEMA_VERSION,
6394 "scenario_id": "golden.live",
6395 "surface_id": "test.surface",
6396 "surface_contract_version": "v1",
6397 "runtime_kind": "live",
6398 "semantics": {
6399 "terminal_outcome": {
6400 "class": "ok",
6401 "severity": "ok",
6402 },
6403 "cancellation": {
6404 "requested": false,
6405 "acknowledged": false,
6406 "cleanup_completed": false,
6407 "finalization_completed": false,
6408 "terminal_phase": "not_cancelled",
6409 },
6410 "loser_drain": {
6411 "applicable": true,
6412 "expected_losers": 2,
6413 "drained_losers": 2,
6414 "status": "complete",
6415 },
6416 "region_close": {
6417 "root_state": "closed",
6418 "quiescent": true,
6419 "live_children": 0,
6420 "finalizers_pending": 0,
6421 "close_completed": true,
6422 },
6423 "obligation_balance": {
6424 "reserved": 0,
6425 "committed": 0,
6426 "aborted": 0,
6427 "leaked": 0,
6428 "unresolved": 0,
6429 "balanced": true,
6430 },
6431 "resource_surface": {
6432 "contract_scope": "test.surface",
6433 "counters": {
6434 "bytes": 128,
6435 "items": 5,
6436 },
6437 "tolerances": {
6438 "bytes": "at_least",
6439 "items": "exact",
6440 },
6441 },
6442 },
6443 "provenance": {
6444 "family": {
6445 "id": "golden.live",
6446 "surface_id": "test.surface",
6447 "surface_contract_version": "v1",
6448 },
6449 "instance": {
6450 "family_id": "golden.live",
6451 "effective_seed": 42,
6452 "runtime_kind": "live",
6453 "run_index": 0,
6454 },
6455 "seed_plan": {
6456 "canonical_seed": 42,
6457 "seed_lineage_id": "golden.live",
6458 "lab_seed_mode": "inherit",
6459 "live_seed_mode": "inherit",
6460 "replay_policy": "single_seed",
6461 },
6462 "effective_seed": 42,
6463 "effective_entropy_seed": derive_component_seed(42, "entropy"),
6464 },
6465 })
6466 );
6467 crate::test_complete!("normalize_live_observable_matches_golden_record_and_manifest");
6468 }
6469
6470 #[test]
6471 fn normalize_and_compare_lab_vs_live() {
6472 init_test("normalize_and_compare_lab_vs_live");
6473 let ident = DualRunScenarioIdentity::phase1("test", "s", "v1", "d", 42);
6474
6475 let report = make_passing_lab_report(42);
6477 let lab_obs = normalize_lab_observable(&ident, &report);
6478
6479 let live_result = run_live_adapter(&ident, |_, _| {});
6481 let live_obs = normalize_live_observable(&ident, &live_result);
6482
6483 let lineage = ident.seed_lineage();
6485 let verdict = compare_observables(&lab_obs, &live_obs, lineage);
6486 assert!(verdict.passed, "Verdict: {}", verdict.summary());
6488 crate::test_complete!("normalize_and_compare_lab_vs_live");
6489 }
6490
6491 #[test]
6492 fn mismatch_summary_with_manifests_includes_capture_sources() {
6493 init_test("mismatch_summary_with_manifests_includes_capture_sources");
6494 let identity = DualRunScenarioIdentity::phase1(
6495 "capture.summary",
6496 "test.surface",
6497 "v1",
6498 "Mismatch summaries should include capture provenance",
6499 42,
6500 );
6501
6502 let mut report = make_passing_lab_report(42);
6503 report.invariant_violations = vec!["obligation leak detected".to_string()];
6504 let (lab_semantics, lab_manifest) = normalize_lab_report(&report, "test.surface");
6505 let lab = NormalizedObservable::new(
6506 &identity,
6507 RuntimeKind::Lab,
6508 lab_semantics,
6509 identity.lab_replay_metadata(),
6510 );
6511
6512 let live_result = run_live_adapter(&identity, |_, witness| {
6513 witness.set_outcome(TerminalOutcome::ok());
6514 });
6515 let live = normalize_live_observable(&identity, &live_result);
6516
6517 let verdict = compare_observables(&lab, &live, identity.seed_lineage());
6518 assert!(!verdict.passed);
6519
6520 let summary = verdict.summary_with_manifests(
6521 Some(&lab_manifest),
6522 Some(&live_result.metadata.capture_manifest),
6523 );
6524 assert!(summary.contains("semantics.terminal_outcome.class"));
6525 assert!(summary.contains("lab_capture=observed via invariant_violations"));
6526 assert!(summary.contains("live_capture=observed via witness.set_outcome"));
6527 crate::test_complete!("mismatch_summary_with_manifests_includes_capture_sources");
6528 }
6529
6530 fn make_test_fuzz_finding(seed: u64) -> crate::lab::fuzz::FuzzFinding {
6533 crate::lab::fuzz::FuzzFinding {
6534 seed,
6535 entropy_seed: 0xFACE,
6536 steps: 500,
6537 violations: vec![],
6538 certificate_hash: 0xABCD,
6539 trace_fingerprint: 0x1234,
6540 minimized_seed: Some(seed.wrapping_add(1)),
6541 }
6542 }
6543
6544 #[test]
6545 fn promote_fuzz_finding_basic() {
6546 init_test("promote_fuzz_finding_basic");
6547 let finding = make_test_fuzz_finding(0xDEAD);
6548 let promoted = promote_fuzz_finding(&finding, "cancellation", "v1");
6549 assert!(promoted.identity.scenario_id.contains("fuzz"));
6550 assert!(promoted.identity.scenario_id.contains("cancellation"));
6551 assert_eq!(promoted.replay_seed, 0xDEAD + 1); assert_eq!(promoted.original_seed, 0xDEAD);
6553 assert_eq!(promoted.identity.seed_plan.canonical_seed, 0xDEAD + 1);
6554 assert_eq!(
6555 promoted.identity.seed_plan.entropy_seed_override,
6556 Some(0xFACE)
6557 );
6558 assert_eq!(promoted.identity.phase, Phase::Phase1);
6559 assert!(promoted.identity.metadata.contains_key("promoted_from"));
6560 crate::test_complete!("promote_fuzz_finding_basic");
6561 }
6562
6563 #[test]
6564 fn promote_fuzz_finding_no_minimized_seed() {
6565 init_test("promote_fuzz_finding_no_minimized_seed");
6566 let mut finding = make_test_fuzz_finding(0xBEEF);
6567 finding.minimized_seed = None;
6568 let promoted = promote_fuzz_finding(&finding, "obligation", "v1");
6569 assert_eq!(promoted.replay_seed, 0xBEEF); crate::test_complete!("promote_fuzz_finding_no_minimized_seed");
6571 }
6572
6573 #[test]
6574 fn promote_fuzz_finding_stabilizes_violation_categories_and_metadata() {
6575 init_test("promote_fuzz_finding_stabilizes_violation_categories_and_metadata");
6576 let mut finding = make_test_fuzz_finding(0xD00D);
6577 finding.violations = vec![
6578 crate::lab::runtime::InvariantViolation::QuiescenceViolation,
6579 crate::lab::runtime::InvariantViolation::Futurelock {
6580 task: crate::types::TaskId::new_for_test(1, 0),
6581 region: crate::types::RegionId::new_for_test(1, 0),
6582 idle_steps: 1,
6583 held: Vec::new(),
6584 },
6585 crate::lab::runtime::InvariantViolation::QuiescenceViolation,
6586 ];
6587
6588 let promoted = promote_fuzz_finding(&finding, "cancellation", "v1");
6589 assert_eq!(
6590 promoted.violation_categories,
6591 vec!["futurelock", "quiescence_violation"]
6592 );
6593 assert!(promoted.identity.scenario_id.contains("futurelock"));
6594 assert_eq!(
6595 promoted.identity.metadata.get("violation_categories"),
6596 Some(&"futurelock,quiescence_violation".to_string())
6597 );
6598 assert_eq!(
6599 promoted.identity.metadata.get("certificate_hash"),
6600 Some(&"0xABCD".to_string())
6601 );
6602 crate::test_complete!("promote_fuzz_finding_stabilizes_violation_categories_and_metadata");
6603 }
6604
6605 #[test]
6606 fn promote_fuzz_finding_repro_command() {
6607 init_test("promote_fuzz_finding_repro_command");
6608 let finding = make_test_fuzz_finding(42);
6609 let promoted = promote_fuzz_finding(&finding, "drain", "v1");
6610 let cmd = promoted.repro_command();
6611 assert!(cmd.contains("rch exec -- env ASUPERSYNC_SEED="));
6612 assert!(cmd.contains("ASUPERSYNC_SEED"));
6613 assert!(cmd.contains("cargo test"));
6614 crate::test_complete!("promote_fuzz_finding_repro_command");
6615 }
6616
6617 #[test]
6618 fn promote_fuzz_finding_display() {
6619 init_test("promote_fuzz_finding_display");
6620 let finding = make_test_fuzz_finding(42);
6621 let promoted = promote_fuzz_finding(&finding, "test", "v1");
6622 let s = format!("{promoted}");
6623 assert!(s.contains("PromotedFuzz"));
6624 crate::test_complete!("promote_fuzz_finding_display");
6625 }
6626
6627 #[test]
6628 fn promote_fuzz_finding_serde_roundtrip() {
6629 init_test("promote_fuzz_finding_serde_roundtrip");
6630 let finding = make_test_fuzz_finding(0xCAFE);
6631 let promoted = promote_fuzz_finding(&finding, "test", "v1")
6632 .with_source_artifact_path("/tmp/fuzz/report.json");
6633 let json = serde_json::to_string_pretty(&promoted).unwrap();
6634 let parsed: PromotedFuzzScenario = serde_json::from_str(&json).unwrap();
6635 assert_eq!(parsed.replay_seed, promoted.replay_seed);
6636 assert_eq!(parsed.original_seed, 0xCAFE);
6637 assert_eq!(
6638 parsed.source_artifact_path.as_deref(),
6639 Some("/tmp/fuzz/report.json")
6640 );
6641 crate::test_complete!("promote_fuzz_finding_serde_roundtrip");
6642 }
6643
6644 #[test]
6645 fn promoted_fuzz_scenario_replay_metadata_includes_artifact_and_repro() {
6646 init_test("promoted_fuzz_scenario_replay_metadata_includes_artifact_and_repro");
6647 let finding = make_test_fuzz_finding(0xCAFE);
6648 let promoted = promote_fuzz_finding(&finding, "test.surface", "v1")
6649 .with_source_artifact_path("/tmp/fuzz/report.json");
6650
6651 let metadata = promoted.lab_replay_metadata();
6652 assert_eq!(metadata.trace_fingerprint, Some(promoted.trace_fingerprint));
6653 assert_eq!(
6654 metadata.artifact_path.as_deref(),
6655 Some("/tmp/fuzz/report.json")
6656 );
6657 assert_eq!(
6658 metadata.repro_command.as_deref(),
6659 Some(promoted.repro_command().as_str())
6660 );
6661 crate::test_complete!("promoted_fuzz_scenario_replay_metadata_includes_artifact_and_repro");
6662 }
6663
6664 #[test]
6665 fn promote_regression_case_basic() {
6666 init_test("promote_regression_case_basic");
6667 let case = crate::lab::fuzz::FuzzRegressionCase {
6668 seed: 0xDEAD,
6669 replay_seed: 0xBEEF,
6670 entropy_seed: 0xCAFE,
6671 certificate_hash: 0x1111,
6672 trace_fingerprint: 0x2222,
6673 violation_categories: vec!["obligation_leak".to_string()],
6674 };
6675 let promoted = promote_regression_case(&case, "obligation", "v1");
6676 assert!(promoted.identity.scenario_id.contains("regression"));
6677 assert_eq!(promoted.replay_seed, 0xBEEF);
6678 assert_eq!(
6679 promoted.identity.seed_plan.entropy_seed_override,
6680 Some(0xCAFE)
6681 );
6682 assert_eq!(promoted.violation_categories, vec!["obligation_leak"]);
6683 crate::test_complete!("promote_regression_case_basic");
6684 }
6685
6686 #[test]
6687 fn promote_regression_corpus_preserves_order() {
6688 init_test("promote_regression_corpus_preserves_order");
6689 let corpus = crate::lab::fuzz::FuzzRegressionCorpus {
6690 schema_version: 1,
6691 base_seed: 42,
6692 entropy_seed: 0x777,
6693 iterations: 1000,
6694 cases: vec![
6695 crate::lab::fuzz::FuzzRegressionCase {
6696 seed: 1,
6697 replay_seed: 10,
6698 entropy_seed: 0x777,
6699 certificate_hash: 0,
6700 trace_fingerprint: 0,
6701 violation_categories: vec!["a".to_string()],
6702 },
6703 crate::lab::fuzz::FuzzRegressionCase {
6704 seed: 2,
6705 replay_seed: 20,
6706 entropy_seed: 0x777,
6707 certificate_hash: 0,
6708 trace_fingerprint: 0,
6709 violation_categories: vec!["b".to_string()],
6710 },
6711 ],
6712 };
6713 let promoted = promote_regression_corpus(&corpus, "test", "v1");
6714 assert_eq!(promoted.len(), 2);
6715 assert_eq!(promoted[0].replay_seed, 10);
6716 assert_eq!(promoted[1].replay_seed, 20);
6717 assert_eq!(promoted[0].campaign_base_seed, Some(42));
6718 assert_eq!(promoted[0].campaign_iteration, Some(0));
6719 assert_eq!(promoted[1].campaign_iteration, Some(1));
6720 assert_eq!(
6721 promoted[0].identity.seed_plan.entropy_seed_override,
6722 Some(0x777)
6723 );
6724 assert_eq!(
6725 promoted[0].identity.metadata.get("campaign_entropy_seed"),
6726 Some(&"0x777".to_string())
6727 );
6728 crate::test_complete!("promote_regression_corpus_preserves_order");
6729 }
6730
6731 #[test]
6732 fn promoted_fuzz_scenario_runs_through_harness() {
6733 init_test("promoted_fuzz_scenario_runs_through_harness");
6734 let finding = make_test_fuzz_finding(42);
6735 let promoted = promote_fuzz_finding(&finding, "test.surface", "v1");
6736
6737 let result = DualRunHarness::from_identity(promoted.identity)
6739 .lab(|_config| make_happy_semantics())
6740 .live(|_seed, _entropy| make_happy_semantics())
6741 .run();
6742
6743 assert!(result.passed());
6744 crate::test_complete!("promoted_fuzz_scenario_runs_through_harness");
6745 }
6746
6747 #[test]
6748 fn promoted_regression_corpus_case_runs_through_harness_with_campaign_metadata() {
6749 init_test("promoted_regression_corpus_case_runs_through_harness_with_campaign_metadata");
6750 let corpus = crate::lab::fuzz::FuzzRegressionCorpus {
6751 schema_version: 1,
6752 base_seed: 0x2A,
6753 entropy_seed: 0x2B,
6754 iterations: 3,
6755 cases: vec![crate::lab::fuzz::FuzzRegressionCase {
6756 seed: 0x10,
6757 replay_seed: 0x11,
6758 entropy_seed: 0x2B,
6759 certificate_hash: 0x2222,
6760 trace_fingerprint: 0x3333,
6761 violation_categories: vec!["obligation_leak".to_string()],
6762 }],
6763 };
6764 let promoted = promote_regression_corpus(&corpus, "test.surface", "v1");
6765 let promoted = promoted[0]
6766 .clone()
6767 .with_source_artifact_path("/tmp/fuzz/corpus.json");
6768
6769 assert_eq!(promoted.campaign_base_seed, Some(0x2A));
6770 assert_eq!(promoted.campaign_iteration, Some(0));
6771 assert_eq!(
6772 promoted.identity.metadata.get("campaign_base_seed"),
6773 Some(&"0x2A".to_string())
6774 );
6775 assert_eq!(
6776 promoted.identity.metadata.get("campaign_iteration"),
6777 Some(&"0".to_string())
6778 );
6779
6780 let metadata = promoted.lab_replay_metadata();
6781 assert_eq!(metadata.trace_fingerprint, Some(0x3333));
6782 assert_eq!(
6783 metadata.artifact_path.as_deref(),
6784 Some("/tmp/fuzz/corpus.json")
6785 );
6786
6787 let result = DualRunHarness::from_identity(promoted.identity)
6788 .lab(|_config| make_happy_semantics())
6789 .live(|_seed, _entropy| make_happy_semantics())
6790 .run();
6791 assert!(result.passed());
6792 crate::test_complete!(
6793 "promoted_regression_corpus_case_runs_through_harness_with_campaign_metadata"
6794 );
6795 }
6796
6797 fn make_test_exploration_report() -> crate::lab::explorer::ExplorationReport {
6798 use crate::lab::explorer::{
6799 CoverageMetrics, RunResult, SaturationMetrics, ViolationReport,
6800 };
6801 use crate::lab::runtime::InvariantViolation;
6802
6803 crate::lab::explorer::ExplorationReport {
6804 total_runs: 3,
6805 unique_classes: 2,
6806 violations: vec![ViolationReport {
6807 seed: 0x20,
6808 steps: 42,
6809 violations: vec![InvariantViolation::QuiescenceViolation],
6810 fingerprint: 0xAAAA,
6811 }],
6812 coverage: CoverageMetrics {
6813 equivalence_classes: 2,
6814 total_runs: 3,
6815 new_class_discoveries: 2,
6816 class_run_counts: BTreeMap::from([(0xAAAA, 2), (0xBBBB, 1)]),
6817 novelty_histogram: BTreeMap::from([(0, 1), (1, 2)]),
6818 saturation: SaturationMetrics {
6819 window: 10,
6820 saturated: false,
6821 existing_class_hits: 1,
6822 runs_since_last_new_class: Some(1),
6823 },
6824 },
6825 top_unexplored: Vec::new(),
6826 runs: vec![
6827 RunResult {
6828 seed: 0x10,
6829 steps: 10,
6830 fingerprint: 0xAAAA,
6831 is_new_class: true,
6832 violations: Vec::new(),
6833 certificate_hash: 0x100,
6834 },
6835 RunResult {
6836 seed: 0x20,
6837 steps: 42,
6838 fingerprint: 0xAAAA,
6839 is_new_class: false,
6840 violations: vec![InvariantViolation::QuiescenceViolation],
6841 certificate_hash: 0x200,
6842 },
6843 RunResult {
6844 seed: 0x30,
6845 steps: 11,
6846 fingerprint: 0xBBBB,
6847 is_new_class: true,
6848 violations: Vec::new(),
6849 certificate_hash: 0x300,
6850 },
6851 ],
6852 }
6853 }
6854
6855 #[test]
6856 fn promote_exploration_report_prefers_lowest_violation_seed_and_preserves_lineage() {
6857 init_test("promote_exploration_report_prefers_lowest_violation_seed_and_preserves_lineage");
6858 let report = make_test_exploration_report();
6859 let promoted = promote_exploration_report(&report, "schedule.surface", "v1");
6860 assert_eq!(promoted.len(), 2);
6861
6862 let promoted_class = promoted
6863 .iter()
6864 .find(|scenario| scenario.trace_fingerprint == 0xAAAA)
6865 .expect("class 0xAAAA should be promoted");
6866 assert_eq!(promoted_class.replay_seed, 0x20);
6867 assert_eq!(promoted_class.original_seeds, vec![0x10, 0x20]);
6868 assert_eq!(promoted_class.violation_seeds, vec![0x20]);
6869 assert_eq!(
6870 promoted_class.supporting_schedule_hashes,
6871 vec![0x100, 0x200]
6872 );
6873 assert!(
6874 promoted_class
6875 .violation_summaries
6876 .iter()
6877 .any(|summary| summary.contains("region closed without quiescence"))
6878 );
6879 assert_eq!(
6880 promoted_class.identity.metadata.get("promoted_from"),
6881 Some(&"exploration_report".to_owned())
6882 );
6883 assert_eq!(
6884 promoted_class
6885 .identity
6886 .metadata
6887 .get("representative_reason"),
6888 Some(&"lowest_violation_seed".to_owned())
6889 );
6890 crate::test_complete!(
6891 "promote_exploration_report_prefers_lowest_violation_seed_and_preserves_lineage"
6892 );
6893 }
6894
6895 #[test]
6896 fn promoted_exploration_scenario_replay_metadata_includes_artifact_and_repro() {
6897 init_test("promoted_exploration_scenario_replay_metadata_includes_artifact_and_repro");
6898 let report = make_test_exploration_report();
6899 let promoted = promote_exploration_report(&report, "schedule.surface", "v1");
6900 let promoted = promoted[0]
6901 .clone()
6902 .with_source_artifact_path("/tmp/dpor/report.json");
6903
6904 let metadata = promoted.lab_replay_metadata();
6905 assert_eq!(metadata.trace_fingerprint, Some(promoted.trace_fingerprint));
6906 assert_eq!(
6907 metadata.schedule_hash,
6908 Some(promoted.representative_schedule_hash)
6909 );
6910 assert_eq!(
6911 metadata.artifact_path.as_deref(),
6912 Some("/tmp/dpor/report.json")
6913 );
6914 assert_eq!(
6915 metadata.repro_command.as_deref(),
6916 Some(promoted.repro_command().as_str())
6917 );
6918 crate::test_complete!(
6919 "promoted_exploration_scenario_replay_metadata_includes_artifact_and_repro"
6920 );
6921 }
6922
6923 #[test]
6924 fn promote_exploration_report_serde_roundtrip() {
6925 init_test("promote_exploration_report_serde_roundtrip");
6926 let report = make_test_exploration_report();
6927 let promoted = promote_exploration_report(&report, "schedule.surface", "v1");
6928 let json = serde_json::to_string_pretty(&promoted).unwrap();
6929 let parsed: Vec<PromotedExplorationScenario> = serde_json::from_str(&json).unwrap();
6930 assert_eq!(parsed.len(), promoted.len());
6931 assert_eq!(parsed[0].trace_fingerprint, promoted[0].trace_fingerprint);
6932 crate::test_complete!("promote_exploration_report_serde_roundtrip");
6933 }
6934}