use mnesis::*;
use std::fmt;
use std::num::NonZeroUsize;
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
struct SId(String);
impl SId {
fn new(v: u64) -> Self {
Self(v.to_string())
}
}
impl fmt::Display for SId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl AsRef<[u8]> for SId {
fn as_ref(&self) -> &[u8] {
self.0.as_bytes()
}
}
#[derive(Debug, Clone, PartialEq)]
enum SEvent {
Tick,
}
impl Message for SEvent {}
impl DomainEvent for SEvent {
fn name(&self) -> &'static str {
"Tick"
}
}
#[derive(Default, Debug, Clone, PartialEq)]
struct SState {
count: u64,
}
impl AggregateState for SState {
type Event = SEvent;
fn initial() -> Self {
Self::default()
}
fn apply(mut self, _: &SEvent) -> Self {
self.count = self.count.wrapping_add(1);
self
}
}
#[derive(Debug, thiserror::Error)]
#[error("e")]
struct SError;
#[derive(Debug)]
struct SAgg;
impl Aggregate for SAgg {
type State = SState;
type Error = SError;
type Id = SId;
}
#[test]
fn c1_version_next_at_max_returns_none() {
let v = Version::new(u64::MAX).expect("u64::MAX is non-zero");
assert!(
v.next().is_none(),
"Version::next() at u64::MAX must return None, not wrap"
);
}
#[test]
fn c1_replay_at_max_version_returns_overflow_error() {
let mut agg = AggregateRoot::<SAgg>::new(SId::new(1));
agg.replay(Version::INITIAL, &SEvent::Tick)
.expect("first replay should succeed");
let max = Version::new(u64::MAX).expect("u64::MAX is non-zero");
assert!(max.next().is_none(), "overflow must be caught");
}
#[test]
fn c2_usize_fits_in_u64() {
assert!(
std::mem::size_of::<usize>() <= std::mem::size_of::<u64>(),
"usize exceeds u64 — version arithmetic will truncate!"
);
}
#[test]
fn c4_version_new_zero_returns_none() {
assert!(
Version::new(0).is_none(),
"Version::new(0) must return None — versions start at 1"
);
}
#[test]
fn c4_replay_rejects_backwards_version() {
let mut agg = AggregateRoot::<SAgg>::new(SId::new(1));
agg.replay(Version::INITIAL, &SEvent::Tick)
.expect("version 1 should succeed");
let v2 = Version::new(2).expect("2 is non-zero");
agg.replay(v2, &SEvent::Tick)
.expect("version 2 should succeed");
let result = agg.replay(Version::INITIAL, &SEvent::Tick);
assert!(
result.is_err(),
"replay must reject non-sequential versions"
);
match result.unwrap_err() {
KernelError::VersionMismatch { expected, actual } => {
let v3 = Version::new(3).expect("3 is non-zero");
assert_eq!(expected, v3);
assert_eq!(actual, Version::INITIAL);
}
other => panic!("expected VersionMismatch, got {other:?}"),
}
}
#[test]
fn c4_replay_rejects_duplicate_version() {
let mut agg = AggregateRoot::<SAgg>::new(SId::new(1));
agg.replay(Version::INITIAL, &SEvent::Tick)
.expect("version 1 should succeed");
let result = agg.replay(Version::INITIAL, &SEvent::Tick);
assert!(result.is_err(), "replay must reject duplicate versions");
match result.unwrap_err() {
KernelError::VersionMismatch { expected, actual } => {
let v2 = Version::new(2).expect("2 is non-zero");
assert_eq!(expected, v2);
assert_eq!(actual, Version::INITIAL);
}
other => panic!("expected VersionMismatch, got {other:?}"),
}
}
#[test]
fn c4_replay_rejects_gap_in_versions() {
let mut agg = AggregateRoot::<SAgg>::new(SId::new(1));
agg.replay(Version::INITIAL, &SEvent::Tick)
.expect("version 1 should succeed");
let v3 = Version::new(3).expect("3 is non-zero");
let result = agg.replay(v3, &SEvent::Tick);
assert!(result.is_err(), "replay must reject version gaps");
match result.unwrap_err() {
KernelError::VersionMismatch { expected, actual } => {
let v2 = Version::new(2).expect("2 is non-zero");
assert_eq!(expected, v2);
assert_eq!(actual, v3);
}
other => panic!("expected VersionMismatch, got {other:?}"),
}
}
#[derive(Debug)]
struct TinyRehydrationAgg;
#[allow(clippy::unwrap_used, reason = "5 is non-zero by inspection")]
impl Aggregate for TinyRehydrationAgg {
type State = SState;
type Error = SError;
type Id = SId;
const MAX_REHYDRATION_EVENTS: NonZeroUsize = NonZeroUsize::new(5).unwrap();
}
#[test]
fn h1_replay_enforces_rehydration_limit() {
let mut agg = AggregateRoot::<TinyRehydrationAgg>::new(SId::new(1));
for i in 1..=5u64 {
let v = Version::new(i).expect("1..=5 are non-zero");
agg.replay(v, &SEvent::Tick).expect("within limit");
}
let v6 = Version::new(6).expect("6 is non-zero");
let result = agg.replay(v6, &SEvent::Tick);
assert!(result.is_err());
match result.unwrap_err() {
KernelError::RehydrationLimitExceeded { max } => {
assert_eq!(max, 5);
}
other => panic!("expected RehydrationLimitExceeded, got {other:?}"),
}
}
#[test]
fn h1_replay_within_limit_succeeds() {
let mut agg = AggregateRoot::<TinyRehydrationAgg>::new(SId::new(1));
for i in 1..=5u64 {
let v = Version::new(i).expect("1..=5 are non-zero");
agg.replay(v, &SEvent::Tick).expect("within limit");
}
let v5 = Version::new(5).expect("5 is non-zero");
assert_eq!(agg.version(), Some(v5));
}
#[test]
fn h1_default_rehydration_limit_is_one_million() {
assert_eq!(SAgg::MAX_REHYDRATION_EVENTS, DEFAULT_MAX_REHYDRATION_EVENTS);
assert_eq!(DEFAULT_MAX_REHYDRATION_EVENTS.get(), 1_000_000);
}
#[test]
fn h2_version_mismatch_contains_expected_and_actual() {
let mut agg = AggregateRoot::<SAgg>::new(SId::new(42));
agg.replay(Version::INITIAL, &SEvent::Tick)
.expect("version 1 should succeed");
let v3 = Version::new(3).expect("3 is non-zero");
let err = agg.replay(v3, &SEvent::Tick).unwrap_err();
match err {
KernelError::VersionMismatch { expected, actual } => {
let v2 = Version::new(2).expect("2 is non-zero");
assert_eq!(expected, v2);
assert_eq!(actual, v3);
}
other => panic!("expected VersionMismatch, got {other:?}"),
}
}
#[test]
fn h2_version_mismatch_display_includes_both_versions() {
let v2 = Version::new(2).expect("2 is non-zero");
let v5 = Version::new(5).expect("5 is non-zero");
let err = KernelError::VersionMismatch {
expected: v2,
actual: v5,
};
let msg = format!("{err}");
assert!(
msg.contains('2'),
"error message must include expected version"
);
assert!(
msg.contains('5'),
"error message must include actual version"
);
}
#[test]
fn h5_replay_panic_no_partial_mutation() {
use std::panic;
#[derive(Debug, Clone)]
enum BombEvent {
Safe,
Explode,
}
impl Message for BombEvent {}
impl DomainEvent for BombEvent {
fn name(&self) -> &'static str {
match self {
Self::Safe => "Safe",
Self::Explode => "Explode",
}
}
}
#[derive(Default, Debug, Clone)]
struct BombState {
count: u64,
}
impl AggregateState for BombState {
type Event = BombEvent;
fn initial() -> Self {
Self::default()
}
fn apply(mut self, event: &BombEvent) -> Self {
match event {
BombEvent::Safe => self.count += 1,
BombEvent::Explode => panic!("state apply panicked"),
}
self
}
}
#[derive(Debug, thiserror::Error)]
#[error("e")]
struct BombError;
#[derive(Debug)]
struct BombAgg;
impl Aggregate for BombAgg {
type State = BombState;
type Error = BombError;
type Id = SId;
}
let mut agg = AggregateRoot::<BombAgg>::new(SId::new(1));
agg.replay(Version::INITIAL, &BombEvent::Safe)
.expect("safe replay should succeed");
assert_eq!(agg.state().count, 1);
assert_eq!(agg.version(), Some(Version::INITIAL));
let v2 = Version::new(2).expect("2 is non-zero");
let result = panic::catch_unwind(panic::AssertUnwindSafe(|| {
let _ = agg.replay(v2, &BombEvent::Explode);
}));
assert!(result.is_err(), "replay should have panicked");
assert_eq!(
agg.state().count,
0,
"state must be left at initial() after panic (no partial mutation)"
);
assert_eq!(
agg.version(),
Some(Version::INITIAL),
"version must not advance after panic in apply"
);
}
#[test]
fn l2_kernel_error_variants_are_known() {
let err = KernelError::VersionMismatch {
expected: Version::INITIAL,
actual: Version::new(2).expect("2 is non-zero"),
};
match &err {
KernelError::VersionMismatch { expected, actual } => {
assert_eq!(*expected, Version::INITIAL);
assert_eq!(actual.as_u64(), 2);
}
KernelError::RehydrationLimitExceeded { max } => {
panic!("wrong variant: RehydrationLimitExceeded(max={max})")
}
KernelError::VersionOverflow => {
panic!("wrong variant: VersionOverflow")
}
other => panic!("unknown new variant: {other:?}"),
}
}
#[test]
fn l2_version_overflow_variant_exists() {
let err = KernelError::VersionOverflow;
let msg = format!("{err}");
assert!(
msg.contains("u64::MAX") || msg.contains("exhausted"),
"VersionOverflow message should mention the limit: {msg}"
);
}
#[test]
fn l2_rehydration_limit_variant_has_max() {
let err = KernelError::RehydrationLimitExceeded { max: 42 };
let msg = format!("{err}");
assert!(
msg.contains("42"),
"RehydrationLimitExceeded message should include the max value: {msg}"
);
}