diff --git a/src/history.rs b/src/history.rs index c6d1dd5..89f0e16 100644 --- a/src/history.rs +++ b/src/history.rs @@ -1812,4 +1812,153 @@ mod tests { "α=0.5 should reach the same fixed point as α=1.0; max_diff={max_diff}" ); } + + #[test] + fn outcome_scores_default_sigma_uses_history_default() { + use crate::Outcome; + + // Path A: explicit sigma=0.5 via override. + let mut h_a = crate::History::builder().score_sigma(0.5).build(); + h_a.add_events([crate::Event { + time: 0_i64, + teams: smallvec::smallvec![ + crate::Team::with_members([crate::Member::new("a")]), + crate::Team::with_members([crate::Member::new("b")]), + ], + outcome: Outcome::scores_with_sigma([3.0, 1.0], 0.5), + }]) + .unwrap(); + h_a.converge().unwrap(); + + // Path B: history-wide default 0.5, no per-event override. + let mut h_b = crate::History::builder().score_sigma(0.5).build(); + h_b.add_events([crate::Event { + time: 0_i64, + teams: smallvec::smallvec![ + crate::Team::with_members([crate::Member::new("a")]), + crate::Team::with_members([crate::Member::new("b")]), + ], + outcome: Outcome::scores([3.0, 1.0]), + }]) + .unwrap(); + h_b.converge().unwrap(); + + // Inheritance: posteriors must be bit-equal. + let curves_a = h_a.learning_curves(); + let curves_b = h_b.learning_curves(); + for (key, a_pts) in curves_a.iter() { + let b_pts = curves_b.get(key).expect("agent missing in path B"); + for (a, b) in a_pts.iter().zip(b_pts.iter()) { + assert_eq!(a.1.pi(), b.1.pi(), "mismatch at agent {key:?}"); + assert_eq!(a.1.tau(), b.1.tau(), "mismatch at agent {key:?}"); + } + } + } + + #[test] + fn outcome_scores_with_sigma_overrides_history_default() { + use crate::Outcome; + + // Path A: history-wide default 0.5, per-event override 2.0. + let mut h_a = crate::History::builder().score_sigma(0.5).build(); + h_a.add_events([crate::Event { + time: 0_i64, + teams: smallvec::smallvec![ + crate::Team::with_members([crate::Member::new("a")]), + crate::Team::with_members([crate::Member::new("b")]), + ], + outcome: Outcome::scores_with_sigma([3.0, 1.0], 2.0), + }]) + .unwrap(); + h_a.converge().unwrap(); + + // Path B: history-wide default 2.0, no per-event override. + let mut h_b = crate::History::builder().score_sigma(2.0).build(); + h_b.add_events([crate::Event { + time: 0_i64, + teams: smallvec::smallvec![ + crate::Team::with_members([crate::Member::new("a")]), + crate::Team::with_members([crate::Member::new("b")]), + ], + outcome: Outcome::scores([3.0, 1.0]), + }]) + .unwrap(); + h_b.converge().unwrap(); + + // Override == default-set-to-the-override-value: bit-equal. + let curves_a = h_a.learning_curves(); + let curves_b = h_b.learning_curves(); + for (key, a_pts) in curves_a.iter() { + let b_pts = curves_b.get(key).expect("agent missing in path B"); + for (a, b) in a_pts.iter().zip(b_pts.iter()) { + assert_eq!(a.1.pi(), b.1.pi(), "mismatch at agent {key:?}"); + assert_eq!(a.1.tau(), b.1.tau(), "mismatch at agent {key:?}"); + } + } + + // Path C: history-wide default 0.5, no override. Different sigma → different posteriors. + let mut h_c = crate::History::builder().score_sigma(0.5).build(); + h_c.add_events([crate::Event { + time: 0_i64, + teams: smallvec::smallvec![ + crate::Team::with_members([crate::Member::new("a")]), + crate::Team::with_members([crate::Member::new("b")]), + ], + outcome: Outcome::scores([3.0, 1.0]), + }]) + .unwrap(); + h_c.converge().unwrap(); + + let curves_c = h_c.learning_curves(); + let mut max_diff: f64 = 0.0; + for (key, a_pts) in curves_a.iter() { + let c_pts = curves_c.get(key).expect("agent missing in path C"); + for (a, c) in a_pts.iter().zip(c_pts.iter()) { + max_diff = max_diff.max((a.1.mu() - c.1.mu()).abs()); + max_diff = max_diff.max((a.1.sigma() - c.1.sigma()).abs()); + } + } + assert!( + max_diff > 1e-6, + "override should produce different posteriors from inherited default; max_diff={max_diff}" + ); + } + + #[test] + fn event_builder_scores_with_sigma_threading() { + use crate::Outcome; + + // Path A: builder fluent API with sigma override. + let mut h_a = crate::History::builder().score_sigma(0.5).build(); + h_a.event(0_i64) + .team(["a"]) + .team(["b"]) + .scores_with_sigma([3.0, 1.0], 2.0) + .commit() + .unwrap(); + h_a.converge().unwrap(); + + // Path B: same outcome via the explicit Outcome constructor. + let mut h_b = crate::History::builder().score_sigma(0.5).build(); + h_b.add_events([crate::Event { + time: 0_i64, + teams: smallvec::smallvec![ + crate::Team::with_members([crate::Member::new("a")]), + crate::Team::with_members([crate::Member::new("b")]), + ], + outcome: Outcome::scores_with_sigma([3.0, 1.0], 2.0), + }]) + .unwrap(); + h_b.converge().unwrap(); + + let curves_a = h_a.learning_curves(); + let curves_b = h_b.learning_curves(); + for (key, a_pts) in curves_a.iter() { + let b_pts = curves_b.get(key).expect("agent missing"); + for (a, b) in a_pts.iter().zip(b_pts.iter()) { + assert_eq!(a.1.pi(), b.1.pi(), "mismatch at agent {key:?}"); + assert_eq!(a.1.tau(), b.1.tau(), "mismatch at agent {key:?}"); + } + } + } }