mnesis 0.1.0

A zero-compromise event-sourcing and CQRS kernel for Rust with maximum compile-time type safety
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
//! Security & reliability tests.
//!
//! These reproduce vulnerabilities found during the mission-critical audit.
//! Each test documents a specific threat and verifies the kernel handles it safely.
//! If any of these tests are removed or weakened, people may die.

use mnesis::*;
use std::fmt;
use std::num::NonZeroUsize;

// --- Minimal test domain ---
#[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;
}

// =============================================================================
// C1: Version overflow — Version::next() at u64::MAX must not silently wrap
// =============================================================================

#[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() {
    // Construct an aggregate already at u64::MAX via replay, then attempt
    // one more replay — must get VersionOverflow.
    // We can't replay u64::MAX events, so we test the VersionOverflow path
    // by replaying version 1, then attempting to advance beyond what's possible.
    // Instead, test that Version::next() returning None is correctly mapped
    // to KernelError::VersionOverflow by the replay method.
    let mut agg = AggregateRoot::<SAgg>::new(SId::new(1));

    // Replay version 1 so the aggregate has a version
    agg.replay(Version::INITIAL, &SEvent::Tick)
        .expect("first replay should succeed");

    // We can't easily get to u64::MAX via replay, but we can verify the
    // Version type itself prevents overflow
    let max = Version::new(u64::MAX).expect("u64::MAX is non-zero");
    assert!(max.next().is_none(), "overflow must be caught");
}

// =============================================================================
// C2: usize as u64 — must not truncate on platforms where usize > 64 bits
// (this test documents the concern; actual truncation only happens on 128-bit)
// =============================================================================

#[test]
fn c2_usize_fits_in_u64() {
    // On current platforms (32/64 bit), usize always fits in u64.
    // This test verifies the assumption holds at compile time.
    // If Rust ever runs on a 128-bit platform, this assertion will fail
    // and force us to handle the conversion properly.
    assert!(
        std::mem::size_of::<usize>() <= std::mem::size_of::<u64>(),
        "usize exceeds u64 — version arithmetic will truncate!"
    );
}

// =============================================================================
// C3: MAX_UNCOMMITTED is removed — no event buffering in AggregateRoot.
// These tests are intentionally omitted. AggregateRoot no longer buffers
// uncommitted events; the repository handles persistence externally.
// =============================================================================

// =============================================================================
// C4: Version::new(0) returns None; replay rejects bad versions
// =============================================================================

#[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");

    // Attempt backwards version — must be rejected
    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");

    // Attempt duplicate version — must be rejected
    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");

    // Skip version 2, jump to 3 — must be rejected
    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:?}"),
    }
}

// =============================================================================
// H1: Unbounded rehydration — large event count
// =============================================================================

// Aggregate with tiny rehydration limit for testing
#[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");
    }
    // 6th event exceeds the limit of 5
    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);
}

// =============================================================================
// H2: Error contents — KernelError::VersionMismatch has expected + actual only
// =============================================================================

#[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");

    // Gap: skip version 2, jump to 3
    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"
    );
}

// =============================================================================
// H5: Panic safety during replay — clone-based preservation
// =============================================================================

#[test]
fn h5_replay_panic_no_partial_mutation() {
    use std::panic;

    // Aggregate with a panicking apply
    #[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");

    // State should be count=1, version=1 after safe replay
    assert_eq!(agg.state().count, 1);
    assert_eq!(agg.version(), Some(Version::INITIAL));

    // Panicking replay: version must not advance, and state is left at
    // initial() — replay moves the state out via mem::replace (no clone), so
    // a clean initial() placeholder remains; the pre-panic value is gone but
    // the state is never partially mutated.
    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"
    );
}

// `h5_apply_events_panic_no_partial_mutation` was relocated in-crate to
// `crates/mnesis/src/aggregate.rs` (`apply_events_mid_batch_panic_leaves_initial_state`)
// when `apply_events` became a private primitive — its panic-safety contract
// (state left at initial(), version untouched) is only reachable in-crate now.

// =============================================================================
// L2: KernelError variants — exhaustive matching
// =============================================================================

#[test]
fn l2_kernel_error_variants_are_known() {
    // If a new variant is added to KernelError, this match must be updated.
    // This forces us to consider the impact on downstream code.
    // KernelError is #[non_exhaustive], so we use a wildcard for future variants.
    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}"
    );
}

// =============================================================================
// M3: Clone and PartialEq on AggregateRoot — REMOVED.
// AggregateRoot no longer implements Clone or PartialEq.
// These tests are intentionally omitted.
// =============================================================================