Skip to main content

mnesis_store_testing/
sequence.rs

1//! Sequence/Protocol conformance: multi-step interactions on one store —
2//! append→read round-trips, optimistic-conflict protocol, `$all` ordering and
3//! resume, and the subscription catch-up→live protocol.
4
5use core::future::Future;
6use core::time::Duration;
7
8use futures::StreamExt;
9use futures::pin_mut;
10use mnesis::Version;
11use mnesis_store::store::RawEventStore;
12use mnesis_store::wake::WakeSource;
13use mnesis_store::{AppendError, Step, StreamKey, Subscription};
14use tokio::time::timeout;
15
16use crate::row::{
17    ConformanceRow, SubId, append_event, append_rows, assert_strictly_increasing, drain_all,
18    drain_stream, envelope_for,
19};
20
21/// Upper bound on any single subscription wait — a hang here means a lost
22/// wake, which is exactly what the check exists to catch.
23const WAIT: Duration = Duration::from_secs(10);
24
25/// A fresh, empty stream reads back empty (absent stream = empty, not error).
26pub async fn check_empty_read_yields_none<S, C, F, Fut>(factory: &F)
27where
28    S: RawEventStore + WakeSource,
29    C: Send,
30    F: Fn() -> Fut + Send + Sync,
31    Fut: Future<Output = (S, C)> + Send,
32{
33    let (store, _guard) = factory().await;
34    let got = drain_stream(&store, &StreamKey::from_slice(b"missing"), Version::INITIAL).await;
35    assert!(
36        got.is_empty(),
37        "reading an absent stream must yield an empty stream, got {} rows",
38        got.len(),
39    );
40}
41
42/// Mixed-shape rows round-trip byte-for-byte in insertion order: Unicode and
43/// dotted event types, schema versions across the u32 range, payloads from
44/// empty through 4 KiB, metadata absent and present.
45pub async fn check_append_then_read_round_trips<S, C, F, Fut>(factory: &F)
46where
47    S: RawEventStore + WakeSource,
48    C: Send,
49    F: Fn() -> Fut + Send + Sync,
50    Fut: Future<Output = (S, C)> + Send,
51{
52    let (store, _guard) = factory().await;
53    let id = StreamKey::from_slice(b"round-trip");
54    let rows = vec![
55        ConformanceRow::new(1, "Created", vec![]),
56        ConformanceRow::new(2, "user.signed_up", vec![0]).with_schema_version(7),
57        ConformanceRow::new(3, "ÉvénementUTF8", vec![0; 64]).with_metadata(vec![1, 2, 3]),
58        ConformanceRow::new(4, "with spaces 123", vec![0xff; 64]).with_schema_version(u32::MAX),
59        ConformanceRow::new(5, "E", (0..=255u8).collect()),
60        ConformanceRow::new(
61            6,
62            "E",
63            (0..4096u32)
64                .map(|i| u8::try_from(i % 256).unwrap_or(0))
65                .collect(),
66        ),
67    ];
68    append_rows(&store, &id, &rows).await;
69    let got = drain_stream(&store, &id, Version::INITIAL).await;
70    assert_eq!(
71        got, rows,
72        "rows must round-trip byte-for-byte in insertion order"
73    );
74}
75
76/// Versions read back strictly monotonic and the stream is fused after `None`.
77pub async fn check_versions_strictly_monotonic_and_fused<S, C, F, Fut>(factory: &F)
78where
79    S: RawEventStore + WakeSource,
80    C: Send,
81    F: Fn() -> Fut + Send + Sync,
82    Fut: Future<Output = (S, C)> + Send,
83{
84    let (store, _guard) = factory().await;
85    let id = StreamKey::from_slice(b"monotonic");
86    let rows: Vec<_> = (1..=64u64)
87        .map(|v| ConformanceRow::new(v, "E", vec![]))
88        .collect();
89    append_rows(&store, &id, &rows).await;
90
91    let stream = store
92        .read_stream(&id, Version::INITIAL)
93        .await
94        .unwrap_or_else(|e| panic!("read_stream failed: {e:?}"));
95    pin_mut!(stream);
96    let mut versions = Vec::new();
97    while let Some(item) = stream.next().await {
98        versions.push(
99            item.unwrap_or_else(|e| panic!("item errored: {e:?}"))
100                .version()
101                .as_u64(),
102        );
103    }
104    let want: Vec<u64> = (1..=64).collect();
105    assert_eq!(
106        versions, want,
107        "versions must be exactly 1..=64, strictly increasing"
108    );
109    for i in 0..8 {
110        assert!(
111            stream.next().await.is_none(),
112            "fused-after-None violated on repeat #{i}",
113        );
114    }
115}
116
117/// A stream larger than any internal batch/refill size (1500 events) drains
118/// completely with no gap or duplicate across the seams.
119pub async fn check_large_stream_completes<S, C, F, Fut>(factory: &F)
120where
121    S: RawEventStore + WakeSource,
122    C: Send,
123    F: Fn() -> Fut + Send + Sync,
124    Fut: Future<Output = (S, C)> + Send,
125{
126    let (store, _guard) = factory().await;
127    let id = StreamKey::from_slice(b"large");
128    let rows: Vec<_> = (1..=1500u64)
129        .map(|v| ConformanceRow::new(v, "E", vec![]))
130        .collect();
131    append_rows(&store, &id, &rows).await;
132    let got = drain_stream(&store, &id, Version::INITIAL).await;
133    let versions: Vec<u64> = got.iter().map(|r| r.version).collect();
134    let want: Vec<u64> = (1..=1500).collect();
135    assert_eq!(
136        versions, want,
137        "1500-event stream must drain exactly 1..=1500"
138    );
139}
140
141/// `read_stream(from)` is INCLUSIVE: from=3 on a 5-event stream yields 3,4,5.
142pub async fn check_read_stream_from_is_inclusive<S, C, F, Fut>(factory: &F)
143where
144    S: RawEventStore + WakeSource,
145    C: Send,
146    F: Fn() -> Fut + Send + Sync,
147    Fut: Future<Output = (S, C)> + Send,
148{
149    let (store, _guard) = factory().await;
150    let id = StreamKey::from_slice(b"inclusive");
151    let rows: Vec<_> = (1..=5u64)
152        .map(|v| ConformanceRow::new(v, "E", vec![]))
153        .collect();
154    append_rows(&store, &id, &rows).await;
155    let got = drain_stream(&store, &id, Version::new(3).expect("v3")).await;
156    let versions: Vec<u64> = got.iter().map(|r| r.version).collect();
157    assert_eq!(
158        versions,
159        vec![3, 4, 5],
160        "read_stream(from=3) is inclusive: yields 3,4,5"
161    );
162}
163
164/// A mismatched `expected_version` surfaces `AppendError::Conflict` carrying
165/// the store's actual head, and the store is untouched.
166pub async fn check_append_conflict_is_surfaced<S, C, F, Fut>(factory: &F)
167where
168    S: RawEventStore + WakeSource,
169    C: Send,
170    F: Fn() -> Fut + Send + Sync,
171    Fut: Future<Output = (S, C)> + Send,
172{
173    let (store, _guard) = factory().await;
174    let id = StreamKey::from_slice(b"conflict");
175    append_rows(
176        &store,
177        &id,
178        &[
179            ConformanceRow::new(1, "E", vec![1]),
180            ConformanceRow::new(2, "E", vec![2]),
181        ],
182    )
183    .await;
184
185    // Stale expectation: stream head is 2, we claim it's still fresh.
186    let env = envelope_for(&ConformanceRow::new(1, "E", vec![9]));
187    let err = store
188        .append(&id, None, &[env])
189        .await
190        .expect_err("appending with a stale expected_version must fail");
191    match err {
192        AppendError::Conflict { actual, .. } => {
193            assert_eq!(
194                actual,
195                Version::new(2),
196                "Conflict must carry the actual head (2)",
197            );
198        }
199        other => panic!("expected Conflict, got {other:?}"),
200    }
201
202    let got = drain_stream(&store, &id, Version::INITIAL).await;
203    assert_eq!(got.len(), 2, "a conflicted append must not land any event");
204    assert_eq!(got[0].payload, vec![1]);
205    assert_eq!(got[1].payload, vec![2]);
206}
207
208/// After a conflict, retrying with the corrected expectation succeeds — the
209/// standard optimistic-concurrency protocol completes.
210pub async fn check_append_retry_after_conflict_succeeds<S, C, F, Fut>(factory: &F)
211where
212    S: RawEventStore + WakeSource,
213    C: Send,
214    F: Fn() -> Fut + Send + Sync,
215    Fut: Future<Output = (S, C)> + Send,
216{
217    let (store, _guard) = factory().await;
218    let id = StreamKey::from_slice(b"retry");
219    append_rows(&store, &id, &[ConformanceRow::new(1, "E", vec![1])]).await;
220
221    // Conflict first…
222    let stale = envelope_for(&ConformanceRow::new(1, "E", vec![9]));
223    store
224        .append(&id, None, &[stale])
225        .await
226        .expect_err("stale append must conflict");
227
228    // …then the corrected retry (head is 1, next event is v2).
229    let retry = envelope_for(&ConformanceRow::new(2, "E", vec![2]));
230    store
231        .append(&id, Version::new(1), &[retry])
232        .await
233        .expect("retry with corrected expected_version must succeed");
234
235    let got = drain_stream(&store, &id, Version::INITIAL).await;
236    let versions: Vec<u64> = got.iter().map(|r| r.version).collect();
237    assert_eq!(versions, vec![1, 2], "retry lands exactly one new event");
238}
239
240/// Empty store: `read_all(None)` yields nothing.
241pub async fn check_all_empty_store_yields_none<S, C, F, Fut>(factory: &F)
242where
243    S: RawEventStore + WakeSource,
244    C: Send,
245    F: Fn() -> Fut + Send + Sync,
246    Fut: Future<Output = (S, C)> + Send,
247{
248    let (store, _guard) = factory().await;
249    let got = drain_all(&store, None).await;
250    assert!(
251        got.is_empty(),
252        "empty store: read_all(None) must yield nothing"
253    );
254}
255
256/// `read_all(None)` yields every event across streams in append (position)
257/// order, positions strictly increasing.
258pub async fn check_all_global_order_across_streams<S, C, F, Fut>(factory: &F)
259where
260    S: RawEventStore + WakeSource,
261    C: Send,
262    F: Fn() -> Fut + Send + Sync,
263    Fut: Future<Output = (S, C)> + Send,
264{
265    let (store, _guard) = factory().await;
266    let a = StreamKey::from_slice(b"a");
267    let b = StreamKey::from_slice(b"b");
268    append_event(&store, &a, 1, b"a1").await;
269    append_event(&store, &a, 2, b"a2").await;
270    append_event(&store, &b, 1, b"b1").await;
271    append_event(&store, &a, 3, b"a3").await;
272    append_event(&store, &a, 4, b"a4").await;
273
274    let got = drain_all(&store, None).await;
275    let payloads: Vec<Vec<u8>> = got.iter().map(|(_, p)| p.clone()).collect();
276    assert_eq!(
277        payloads,
278        vec![
279            b"a1".to_vec(),
280            b"a2".to_vec(),
281            b"b1".to_vec(),
282            b"a3".to_vec(),
283            b"a4".to_vec()
284        ],
285        "read_all(None) must yield every event across streams in append order",
286    );
287    assert_strictly_increasing(&got);
288}
289
290/// `read_all(Some(p))` is EXCLUSIVE: strictly after `p`.
291pub async fn check_all_from_is_exclusive<S, C, F, Fut>(factory: &F)
292where
293    S: RawEventStore + WakeSource,
294    C: Send,
295    F: Fn() -> Fut + Send + Sync,
296    Fut: Future<Output = (S, C)> + Send,
297{
298    let (store, _guard) = factory().await;
299    let a = StreamKey::from_slice(b"a");
300    append_event(&store, &a, 1, b"a1").await;
301    append_event(&store, &a, 2, b"a2").await;
302    append_event(&store, &a, 3, b"a3").await;
303
304    let full = drain_all(&store, None).await;
305    assert_eq!(full.len(), 3);
306    let checkpoint = full[0].0;
307
308    let rest = drain_all(&store, Some(checkpoint)).await;
309    let payloads: Vec<Vec<u8>> = rest.iter().map(|(_, p)| p.clone()).collect();
310    assert_eq!(
311        payloads,
312        vec![b"a2".to_vec(), b"a3".to_vec()],
313        "read_all(Some(p)) is EXCLUSIVE",
314    );
315    assert!(
316        rest[0].0 > checkpoint,
317        "resumed position must be strictly after checkpoint"
318    );
319}
320
321/// Multi-resume cycles reconstruct the single-shot read exactly — no gap,
322/// duplicate, or skip across the seams.
323pub async fn check_all_multi_resume_cycles<S, C, F, Fut>(factory: &F)
324where
325    S: RawEventStore + WakeSource,
326    C: Send,
327    F: Fn() -> Fut + Send + Sync,
328    Fut: Future<Output = (S, C)> + Send,
329{
330    let (store, _guard) = factory().await;
331    let a = StreamKey::from_slice(b"a");
332    let b = StreamKey::from_slice(b"b");
333    let mut va = 0u64;
334    let mut vb = 0u64;
335    let mut expected: Vec<Vec<u8>> = Vec::new();
336    for i in 0..10u64 {
337        if i % 2 == 0 {
338            va += 1;
339            let p = format!("a{va}").into_bytes();
340            append_event(&store, &a, va, &p).await;
341            expected.push(p);
342        } else {
343            vb += 1;
344            let p = format!("b{vb}").into_bytes();
345            append_event(&store, &b, vb, &p).await;
346            expected.push(p);
347        }
348    }
349
350    let full = drain_all(&store, None).await;
351    let full_payloads: Vec<Vec<u8>> = full.iter().map(|(_, p)| p.clone()).collect();
352    assert_eq!(
353        full_payloads, expected,
354        "single-shot read_all(None) must match append order"
355    );
356
357    let mut acc: Vec<(S::AllPosition, Vec<u8>)> = Vec::new();
358    let mut checkpoint: Option<S::AllPosition> = None;
359    loop {
360        let stream = store
361            .read_all(checkpoint)
362            .await
363            .unwrap_or_else(|e| panic!("open read_all cycle failed: {e:?}"));
364        pin_mut!(stream);
365        let mut taken = 0;
366        let mut advanced = false;
367        while let Some(item) = stream.next().await {
368            let (pos, env) = item.unwrap_or_else(|e| panic!("cycle item errored: {e:?}"));
369            acc.push((pos, env.payload().to_vec()));
370            checkpoint = Some(pos);
371            advanced = true;
372            taken += 1;
373            if taken == 3 {
374                break;
375            }
376        }
377        if !advanced {
378            break;
379        }
380    }
381
382    let acc_payloads: Vec<Vec<u8>> = acc.iter().map(|(_, p)| p.clone()).collect();
383    assert_eq!(
384        acc_payloads, full_payloads,
385        "multi-resume cycles must reconstruct the full stream exactly",
386    );
387    assert_strictly_increasing(&acc);
388}
389
390/// `read_all(Some(last))` is empty at the boundary; a later append surfaces
391/// exactly the new event from the same checkpoint.
392pub async fn check_all_boundary_then_new_append<S, C, F, Fut>(factory: &F)
393where
394    S: RawEventStore + WakeSource,
395    C: Send,
396    F: Fn() -> Fut + Send + Sync,
397    Fut: Future<Output = (S, C)> + Send,
398{
399    let (store, _guard) = factory().await;
400    let a = StreamKey::from_slice(b"a");
401    let b = StreamKey::from_slice(b"b");
402    append_event(&store, &a, 1, b"a1").await;
403    append_event(&store, &b, 1, b"b1").await;
404
405    let full = drain_all(&store, None).await;
406    assert_eq!(full.len(), 2);
407    let last = full.last().expect("non-empty").0;
408
409    let empty = drain_all(&store, Some(last)).await;
410    assert!(
411        empty.is_empty(),
412        "nothing is strictly after the last position"
413    );
414
415    append_event(&store, &a, 2, b"a2").await;
416    let after = drain_all(&store, Some(last)).await;
417    let payloads: Vec<Vec<u8>> = after.iter().map(|(_, p)| p.clone()).collect();
418    assert_eq!(
419        payloads,
420        vec![b"a2".to_vec()],
421        "same checkpoint surfaces exactly the new event"
422    );
423    assert!(
424        after[0].0 > last,
425        "new position must be strictly after the prior last"
426    );
427}
428
429/// Inclusive `read_stream` and exclusive `read_all` coexist on one store —
430/// the intentional asymmetry (CLAUDE rule 4).
431pub async fn check_read_stream_inclusive_read_all_exclusive_coexist<S, C, F, Fut>(factory: &F)
432where
433    S: RawEventStore + WakeSource,
434    C: Send,
435    F: Fn() -> Fut + Send + Sync,
436    Fut: Future<Output = (S, C)> + Send,
437{
438    let (store, _guard) = factory().await;
439    let a = StreamKey::from_slice(b"a");
440    append_event(&store, &a, 1, b"a1").await;
441    append_event(&store, &a, 2, b"a2").await;
442    append_event(&store, &a, 3, b"a3").await;
443
444    let got = drain_stream(&store, &a, Version::new(2).expect("v2")).await;
445    let versions: Vec<u64> = got.iter().map(|r| r.version).collect();
446    assert_eq!(versions, vec![2, 3], "read_stream(from=2) is INCLUSIVE");
447
448    let full = drain_all(&store, None).await;
449    assert_eq!(full.len(), 3);
450    let pos_of_a2 = full[1].0;
451    let after = drain_all(&store, Some(pos_of_a2)).await;
452    let payloads: Vec<Vec<u8>> = after.iter().map(|(_, p)| p.clone()).collect();
453    assert_eq!(
454        payloads,
455        vec![b"a3".to_vec()],
456        "read_all(from=pos(a2)) is EXCLUSIVE"
457    );
458}
459
460/// Take the next subscription item within `WAIT`, panicking on hang, stream
461/// end, or read error. Returns the `Step`.
462async fn next_step<St, T, E>(stream: &mut core::pin::Pin<&mut St>, what: &str) -> Step<T>
463where
464    St: futures::Stream<Item = Result<Step<T>, E>>,
465    E: core::fmt::Debug,
466{
467    timeout(WAIT, stream.next())
468        .await
469        .unwrap_or_else(|_| panic!("{what}: subscription hung (lost wake?)"))
470        .unwrap_or_else(|| panic!("{what}: subscription ended (must never return None)"))
471        .unwrap_or_else(|e| panic!("{what}: subscription item errored: {e:?}"))
472}
473
474/// Per-stream subscription protocol: backlog in order, then `CaughtUp`
475/// exactly once, then live events.
476pub async fn check_subscription_backlog_then_caught_up_then_live<S, C, F, Fut>(factory: &F)
477where
478    S: RawEventStore + WakeSource,
479    S::Stream: Unpin,
480    C: Send,
481    F: Fn() -> Fut + Send + Sync,
482    Fut: Future<Output = (S, C)> + Send,
483{
484    let (raw, _guard) = factory().await;
485    let store = raw.into_store();
486    let id = SubId::new("sub-proto");
487    for v in 1..=3u64 {
488        append_event(&store, &id.key(), v, format!("p{v}").as_bytes()).await;
489    }
490
491    let sub = Subscription::new(&store);
492    let stream = sub
493        .subscribe(&id, None)
494        .unwrap_or_else(|e| panic!("register failed: {e:?}"));
495    pin_mut!(stream);
496
497    for want in 1..=3u64 {
498        match next_step(&mut stream, "backlog").await {
499            Step::Event(env) => assert_eq!(
500                env.version().as_u64(),
501                want,
502                "backlog must replay in version order",
503            ),
504            Step::CaughtUp => panic!("CaughtUp before the backlog drained (at v{want})"),
505        }
506    }
507    match next_step(&mut stream, "boundary").await {
508        Step::CaughtUp => {}
509        Step::Event(env) => panic!(
510            "expected CaughtUp after backlog, got Event v{}",
511            env.version()
512        ),
513    }
514
515    // Live phase: an append after CaughtUp is delivered.
516    append_event(&store, &id.key(), 4, b"p4").await;
517    match next_step(&mut stream, "live").await {
518        Step::Event(env) => assert_eq!(env.version().as_u64(), 4, "live event must be v4"),
519        Step::CaughtUp => panic!("CaughtUp must be emitted exactly once"),
520    }
521}
522
523/// `subscribe(Some(v))` resumes STRICTLY AFTER `v` — no duplicate of the
524/// checkpointed event.
525pub async fn check_subscription_resume_strict_after<S, C, F, Fut>(factory: &F)
526where
527    S: RawEventStore + WakeSource,
528    S::Stream: Unpin,
529    C: Send,
530    F: Fn() -> Fut + Send + Sync,
531    Fut: Future<Output = (S, C)> + Send,
532{
533    let (raw, _guard) = factory().await;
534    let store = raw.into_store();
535    let id = SubId::new("sub-resume");
536    for v in 1..=5u64 {
537        append_event(&store, &id.key(), v, b"p").await;
538    }
539
540    let sub = Subscription::new(&store);
541    let stream = sub
542        .subscribe(&id, Some(Version::new(3).expect("v3")))
543        .unwrap_or_else(|e| panic!("register failed: {e:?}"));
544    pin_mut!(stream);
545
546    match next_step(&mut stream, "resume").await {
547        Step::Event(env) => assert_eq!(
548            env.version().as_u64(),
549            4,
550            "resume from Some(3) must deliver v4 first (strict-after, no dup)",
551        ),
552        Step::CaughtUp => panic!("expected v4 before CaughtUp"),
553    }
554}
555
556/// `$all` subscription protocol: cross-stream backlog in position order, then
557/// `CaughtUp` exactly once, then live events with strictly increasing tags.
558pub async fn check_subscription_all_backlog_then_caught_up_then_live<S, C, F, Fut>(factory: &F)
559where
560    S: RawEventStore + WakeSource,
561    S::AllStream: Unpin,
562    C: Send,
563    F: Fn() -> Fut + Send + Sync,
564    Fut: Future<Output = (S, C)> + Send,
565{
566    let (raw, _guard) = factory().await;
567    let store = raw.into_store();
568    let a = StreamKey::from_slice(b"a");
569    let b = StreamKey::from_slice(b"b");
570    append_event(&store, &a, 1, b"a1").await;
571    append_event(&store, &b, 1, b"b1").await;
572    append_event(&store, &a, 2, b"a2").await;
573
574    let sub = Subscription::new(&store);
575    let stream = sub
576        .subscribe_all(None)
577        .unwrap_or_else(|e| panic!("register failed: {e:?}"));
578    pin_mut!(stream);
579
580    let mut backlog: Vec<(S::AllPosition, Vec<u8>)> = Vec::new();
581    while let Step::Event((pos, env)) = next_step(&mut stream, "all backlog").await {
582        backlog.push((pos, env.payload().to_vec()));
583    }
584    let payloads: Vec<Vec<u8>> = backlog.iter().map(|(_, p)| p.clone()).collect();
585    assert_eq!(
586        payloads,
587        vec![b"a1".to_vec(), b"b1".to_vec(), b"a2".to_vec()],
588        "$all backlog must replay in position order",
589    );
590    assert_strictly_increasing(&backlog);
591    let last = backlog.last().expect("non-empty").0;
592
593    append_event(&store, &b, 2, b"b2").await;
594    match next_step(&mut stream, "all live").await {
595        Step::Event((pos, env)) => {
596            assert_eq!(
597                env.payload(),
598                b"b2",
599                "live $all event must be the new append"
600            );
601            assert!(
602                pos > last,
603                "live position must be strictly after the backlog"
604            );
605        }
606        Step::CaughtUp => panic!("CaughtUp must be emitted exactly once"),
607    }
608}
609
610/// Subscribing to a stream that does not exist yet parks (after `CaughtUp`)
611/// and is woken by the stream's FIRST event — the producer-after-consumer
612/// startup order must work.
613pub async fn check_subscription_absent_stream_waits_then_delivers<S, C, F, Fut>(factory: &F)
614where
615    S: RawEventStore + WakeSource,
616    S::Stream: Unpin,
617    C: Send,
618    F: Fn() -> Fut + Send + Sync,
619    Fut: Future<Output = (S, C)> + Send,
620{
621    let (raw, _guard) = factory().await;
622    let store = raw.into_store();
623    let id = SubId::new("ghost");
624
625    let sub = Subscription::new(&store);
626    let stream = sub
627        .subscribe(&id, None)
628        .unwrap_or_else(|e| panic!("register failed: {e:?}"));
629    pin_mut!(stream);
630
631    // An absent stream has an empty backlog: CaughtUp arrives first.
632    match next_step(&mut stream, "absent-stream boundary").await {
633        Step::CaughtUp => {}
634        Step::Event(env) => panic!("absent stream must have no backlog, got v{}", env.version()),
635    }
636
637    // The FIRST event ever written to the stream wakes the parked subscriber.
638    append_event(&store, &id.key(), 1, b"first").await;
639    match next_step(&mut stream, "absent-stream first event").await {
640        Step::Event(env) => {
641            assert_eq!(env.version().as_u64(), 1, "the first event must be v1");
642            assert_eq!(env.payload(), b"first");
643        }
644        Step::CaughtUp => panic!("CaughtUp must be emitted exactly once"),
645    }
646}
647
648/// Two simultaneous subscribers on ONE stream each receive the full event
649/// sequence — subscriptions are fan-out, never competing-consumer queues.
650pub async fn check_two_subscribers_same_stream_both_receive<S, C, F, Fut>(factory: &F)
651where
652    S: RawEventStore + WakeSource,
653    S::Stream: Unpin,
654    C: Send,
655    F: Fn() -> Fut + Send + Sync,
656    Fut: Future<Output = (S, C)> + Send,
657{
658    let (raw, _guard) = factory().await;
659    let store = raw.into_store();
660    let id = SubId::new("fanout");
661    append_event(&store, &id.key(), 1, b"p1").await;
662
663    let sub = Subscription::new(&store);
664    let stream_a = sub
665        .subscribe(&id, None)
666        .unwrap_or_else(|e| panic!("register a failed: {e:?}"));
667    let stream_b = sub
668        .subscribe(&id, None)
669        .unwrap_or_else(|e| panic!("register b failed: {e:?}"));
670    pin_mut!(stream_a);
671    pin_mut!(stream_b);
672
673    // Both drain the backlog and reach CaughtUp independently.
674    match next_step(&mut stream_a, "fanout backlog a").await {
675        Step::Event(env) => assert_eq!(env.version().as_u64(), 1, "subscriber a backlog"),
676        Step::CaughtUp => panic!("subscriber a: CaughtUp before backlog"),
677    }
678    match next_step(&mut stream_a, "fanout boundary a").await {
679        Step::CaughtUp => {}
680        Step::Event(env) => panic!("subscriber a: expected CaughtUp, got v{}", env.version()),
681    }
682    match next_step(&mut stream_b, "fanout backlog b").await {
683        Step::Event(env) => assert_eq!(env.version().as_u64(), 1, "subscriber b backlog"),
684        Step::CaughtUp => panic!("subscriber b: CaughtUp before backlog"),
685    }
686    match next_step(&mut stream_b, "fanout boundary b").await {
687        Step::CaughtUp => {}
688        Step::Event(env) => panic!("subscriber b: expected CaughtUp, got v{}", env.version()),
689    }
690
691    // One live append reaches BOTH subscribers.
692    append_event(&store, &id.key(), 2, b"p2").await;
693    match next_step(&mut stream_a, "fanout live a").await {
694        Step::Event(env) => assert_eq!(
695            env.version().as_u64(),
696            2,
697            "subscriber a must receive the live event — fan-out, not a queue",
698        ),
699        Step::CaughtUp => panic!("subscriber a: CaughtUp must be emitted exactly once"),
700    }
701    match next_step(&mut stream_b, "fanout live b").await {
702        Step::Event(env) => assert_eq!(
703            env.version().as_u64(),
704            2,
705            "subscriber b must receive the live event — fan-out, not a queue",
706        ),
707        Step::CaughtUp => panic!("subscriber b: CaughtUp must be emitted exactly once"),
708    }
709}
710
711/// A backlog larger than the catch-up chunk (1024) crosses the internal
712/// rescan seams with no gap or duplicate, and `CaughtUp` still arrives.
713pub async fn check_subscription_large_backlog_crosses_chunk_seam<S, C, F, Fut>(factory: &F)
714where
715    S: RawEventStore + WakeSource,
716    S::Stream: Unpin,
717    C: Send,
718    F: Fn() -> Fut + Send + Sync,
719    Fut: Future<Output = (S, C)> + Send,
720{
721    const N: u64 = 2500; // > 2 × CATCHUP_CHUNK (1024)
722    let (raw, _guard) = factory().await;
723    let store = raw.into_store();
724    let id = SubId::new("sub-chunk");
725    let rows: Vec<_> = (1..=N)
726        .map(|v| ConformanceRow::new(v, "E", vec![]))
727        .collect();
728    append_rows(&store, &id.key(), &rows).await;
729
730    let sub = Subscription::new(&store);
731    let stream = sub
732        .subscribe(&id, None)
733        .unwrap_or_else(|e| panic!("register failed: {e:?}"));
734    pin_mut!(stream);
735
736    let mut versions = Vec::with_capacity(usize::try_from(N).unwrap_or(usize::MAX));
737    while let Step::Event(env) = next_step(&mut stream, "chunk backlog").await {
738        versions.push(env.version().as_u64());
739    }
740    let want: Vec<u64> = (1..=N).collect();
741    assert_eq!(
742        versions, want,
743        "backlog across chunk seams must be exactly 1..=N — no gap, no duplicate",
744    );
745}
746
747/// A subscription opened beyond the head filters below-bound live appends.
748///
749/// `subscribe(Some(v))` with `v` past the current head parks after an empty
750/// backlog; live appends at versions **at or below** `v` wake the loop but
751/// must never be delivered — the first delivered event is `v + 1`. A loop or
752/// adapter that rescans from the wrong position after a below-bound wake
753/// would surface one of the filtered events here.
754pub async fn check_subscription_beyond_head_filters_below_bound<S, C, F, Fut>(factory: &F)
755where
756    S: RawEventStore + WakeSource,
757    S::Stream: Unpin,
758    C: Send,
759    F: Fn() -> Fut + Send + Sync,
760    Fut: Future<Output = (S, C)> + Send,
761{
762    let (raw, _guard) = factory().await;
763    let store = raw.into_store();
764    let id = SubId::new("beyond-head");
765    append_event(&store, &id.key(), 1, b"p1").await;
766    append_event(&store, &id.key(), 2, b"p2").await;
767
768    // Head is 2; subscribe strictly after 5 — the backlog is empty.
769    let sub = Subscription::new(&store);
770    let stream = sub
771        .subscribe(&id, Some(Version::new(5).expect("v5")))
772        .unwrap_or_else(|e| panic!("register failed: {e:?}"));
773    pin_mut!(stream);
774    match next_step(&mut stream, "beyond-head boundary").await {
775        Step::CaughtUp => {}
776        Step::Event(env) => panic!(
777            "subscribing beyond the head must have an empty backlog, got v{}",
778            env.version(),
779        ),
780    }
781
782    // Live appends at v3..=v5 are all <= from: each wakes the loop, none may
783    // be delivered. v6 is the first version strictly after `from`.
784    for v in 3..=6u64 {
785        append_event(&store, &id.key(), v, format!("p{v}").as_bytes()).await;
786    }
787    match next_step(&mut stream, "beyond-head first delivery").await {
788        Step::Event(env) => assert_eq!(
789            env.version().as_u64(),
790            6,
791            "the first delivered event must be from+1 (6) — below-bound live appends must be filtered, never delivered",
792        ),
793        Step::CaughtUp => panic!("CaughtUp must be emitted exactly once"),
794    }
795}