1use std::time::Duration;
2
3use async_trait::async_trait;
4use cortexkit_bus_trait::{
5 map_connection_failure, map_credential_sign_failure, terminally_dispose, BackendKind, BusError,
6 ClaimOutcome, ConformanceBackend, ConnectionFailure, ContentDigest, CredentialSignFailure,
7 DurableOwner, Headers, PropertyOutcome, PropertySpec, Register, RegisterUpdate, RegisterWatch,
8 Stream, StreamCursor, WatchEvent, WorkQueue,
9};
10
11use crate::{BackendEvent, InMemoryBus, InMemoryConfig, WorkQueueConfig};
12
13pub struct InMemoryConformance;
14
15#[async_trait]
16impl ConformanceBackend for InMemoryConformance {
17 fn backend_kind(&self) -> BackendKind {
18 BackendKind::InMemory
19 }
20
21 async fn probe(&self, property: PropertySpec) -> PropertyOutcome {
22 let result = match property.name {
23 "publish_ack_monotonic_sequence" => publish_ack_sequence().await,
24 "consumer_replay_within_process_lifetime" => replay_from_cursor().await,
25 "stream_delivery_count_rises_on_each_nak" => stream_delivery_count().await,
26 "stream_term_is_never_redelivered" => stream_term().await,
27 "stream_in_progress_extends_ack_wait" => stream_in_progress().await,
28 "register_revision" => register_revision().await,
29 "register_absent_vs_empty" => register_absent_vs_empty().await,
30 "watch_snapshot_then_updates_with_connection_flag" => register_watch().await,
31 "work_redelivery_count" => work_redelivery().await,
32 "work_cap_exhaustion_and_dead_letter" => cap_exhaustion().await,
33 "full_work_queue_names_exhausted_limit" => full_queue().await,
34 "error_taxonomy_and_boundary_mapping" => error_taxonomy().await,
35 other => Err(format!("in-memory has no parity probe named {other}")),
36 };
37 match result {
38 Ok(()) => PropertyOutcome::Passed,
39 Err(reason) => PropertyOutcome::Failed { reason },
40 }
41 }
42}
43
44async fn publish_ack_sequence() -> Result<(), String> {
45 let bus = InMemoryBus::new(InMemoryConfig::default());
46 let first = bus
47 .publish("ck.box.room.a.post", "one", digest("one"), Headers::new())
48 .await
49 .map_err(debug)?;
50 let second = bus
51 .publish("ck.box.room.a.post", "two", digest("two"), Headers::new())
52 .await
53 .map_err(debug)?;
54 require(
55 !first.duplicate()
56 && !second.duplicate()
57 && second.stream_sequence() > first.stream_sequence(),
58 "publish sequence did not increase",
59 )
60}
61
62async fn replay_from_cursor() -> Result<(), String> {
63 let bus =
64 InMemoryBus::new(InMemoryConfig::default().with_durable(DurableOwner::Agent("agent")));
65 let receipt = bus
66 .publish(
67 "ck.box.peer.agent.s.deliver",
68 "m1",
69 digest("m1"),
70 Headers::new(),
71 )
72 .await
73 .map_err(debug)?;
74 require(!receipt.duplicate(), "unexpected duplicate publish")?;
75 let expected = bus
76 .consumer(DurableOwner::Agent("agent"))
77 .await
78 .map_err(debug)?
79 .next()
80 .await
81 .map_err(debug)?
82 .ok_or("first cursor saw no message")?;
83 let mut reopened = bus
84 .consumer(DurableOwner::Agent("agent"))
85 .await
86 .map_err(debug)?;
87 let replayed = reopened
88 .next()
89 .await
90 .map_err(debug)?
91 .ok_or("reopened cursor saw no replay")?;
92 require(
93 expected.stream_seq == replayed.stream_seq,
94 "cursor did not replay",
95 )?;
96 reopened.ack().await.map_err(debug)
97}
98
99const STREAM_SUBJECT: &str = "ck.box.peer.agent.s.deliver";
100
101async fn stream_delivery_count() -> Result<(), String> {
102 let bus =
103 InMemoryBus::new(InMemoryConfig::default().with_durable(DurableOwner::Agent("agent")));
104 let receipt = bus
105 .publish(STREAM_SUBJECT, "m1", digest("m1"), Headers::new())
106 .await
107 .map_err(debug)?;
108 require(!receipt.duplicate(), "unexpected duplicate publish")?;
109 let mut cursor = bus
110 .consumer(DurableOwner::Agent("agent"))
111 .await
112 .map_err(debug)?;
113 for expected in 1..=3 {
114 let delivery = cursor
115 .next()
116 .await
117 .map_err(debug)?
118 .ok_or("a nak'd message was not redelivered")?;
119 require(
120 delivery.delivery_count == expected,
121 &format!(
122 "delivery {expected} reported count {}",
123 delivery.delivery_count
124 ),
125 )?;
126 if expected < 3 {
127 cursor.nak(Duration::ZERO).await.map_err(debug)?;
128 }
129 }
130 cursor.ack().await.map_err(debug)
131}
132
133async fn stream_term() -> Result<(), String> {
134 let bus =
135 InMemoryBus::new(InMemoryConfig::default().with_durable(DurableOwner::Agent("agent")));
136 for id in ["m1", "m2"] {
137 let receipt = bus
138 .publish(STREAM_SUBJECT, id, digest(id), Headers::new())
139 .await
140 .map_err(debug)?;
141 require(!receipt.duplicate(), "unexpected duplicate publish")?;
142 }
143 let mut cursor = bus
144 .consumer(DurableOwner::Agent("agent"))
145 .await
146 .map_err(debug)?;
147 let first = cursor
148 .next()
149 .await
150 .map_err(debug)?
151 .ok_or("first message was not delivered")?;
152 require(first.message.id == "m1", "first delivery was not m1")?;
153 cursor.term(Some("body_absent")).await.map_err(debug)?;
154 let second = cursor
155 .next()
156 .await
157 .map_err(debug)?
158 .ok_or("the message after a term was not delivered")?;
159 require(
160 second.message.id == "m2" && second.delivery_count == 1,
161 "a terminated message was redelivered",
162 )?;
163 cursor.ack().await.map_err(debug)?;
164 let mut reopened = bus
165 .consumer(DurableOwner::Agent("agent"))
166 .await
167 .map_err(debug)?;
168 require(
169 reopened.next().await.map_err(debug)?.is_none(),
170 "a terminated message was redelivered to a reopened cursor",
171 )?;
172 require(
173 bus.events().iter().any(|event| {
174 matches!(
175 event,
176 BackendEvent::StreamTerminated { stream_seq, reason: Some(reason) }
177 if *stream_seq == first.stream_seq && reason == "body_absent"
178 )
179 }),
180 "the termination and its reason were not recorded",
181 )
182}
183
184async fn stream_in_progress() -> Result<(), String> {
185 let ack_wait = Duration::from_millis(200);
186 let bus = InMemoryBus::new(
187 InMemoryConfig::default()
188 .with_durable(DurableOwner::Agent("agent"))
189 .with_stream_ack_wait(ack_wait),
190 );
191 for id in ["m1", "m2"] {
192 let receipt = bus
193 .publish(STREAM_SUBJECT, id, digest(id), Headers::new())
194 .await
195 .map_err(debug)?;
196 require(!receipt.duplicate(), "unexpected duplicate publish")?;
197 }
198 let mut slow = bus
199 .consumer(DurableOwner::Agent("agent"))
200 .await
201 .map_err(debug)?;
202 let mut other = bus
203 .consumer(DurableOwner::Agent("agent"))
204 .await
205 .map_err(debug)?;
206
207 slow.next()
210 .await
211 .map_err(debug)?
212 .ok_or("m1 was not delivered")?;
213 tokio::time::sleep(ack_wait + Duration::from_millis(100)).await;
214 let redelivered = other
215 .next()
216 .await
217 .map_err(debug)?
218 .ok_or("m1 was not redelivered after the ack wait")?;
219 require(
220 redelivered.message.id == "m1" && redelivered.delivery_count == 2,
221 "the ack-wait redelivery was not m1's second delivery",
222 )?;
223 other.ack().await.map_err(debug)?;
224 let _ = slow.ack().await;
225
226 slow.next()
229 .await
230 .map_err(debug)?
231 .ok_or("m2 was not delivered")?;
232 tokio::time::sleep(ack_wait * 3 / 5).await;
233 slow.in_progress().await.map_err(debug)?;
234 tokio::time::sleep(ack_wait * 3 / 5).await;
235 require(
236 other.next().await.map_err(debug)?.is_none(),
237 "a message reported in progress was redelivered past the original ack wait",
238 )?;
239 slow.ack().await.map_err(debug)?;
240 require(
241 other.next().await.map_err(debug)?.is_none(),
242 "an acknowledged message was redelivered",
243 )
244}
245
246async fn register_revision() -> Result<(), String> {
247 let bus = InMemoryBus::new(InMemoryConfig::default());
248 let first = bus.put("module", b"one".to_vec()).await.map_err(debug)?;
249 let second = bus.put("module", b"two".to_vec()).await.map_err(debug)?;
250 let read = bus.get("module").await.map_err(debug)?;
251 require(second > first, "register revision did not increase")?;
252 require(read.revision == second, "get returned the wrong revision")
253}
254
255async fn register_absent_vs_empty() -> Result<(), String> {
256 let bus = InMemoryBus::new(InMemoryConfig::default());
257 bus.put("empty", Vec::new()).await.map_err(debug)?;
258 let empty = bus.get("empty").await.map_err(debug)?;
259 require(empty.value.is_empty(), "empty value was not preserved")?;
260 require(
261 matches!(bus.get("missing").await, Err(BusError::Absent { .. })),
262 "missing key was not Absent",
263 )
264}
265
266async fn register_watch() -> Result<(), String> {
267 let bus = InMemoryBus::new(InMemoryConfig::default());
268 bus.put("module.a", b"v1".to_vec()).await.map_err(debug)?;
269 let mut watch = bus.watch("module.").await.map_err(debug)?;
270 require(
271 !watch.has_snapshot(),
272 "watch claimed a snapshot before delivery",
273 )?;
274 let snapshot = watch.next().await.map_err(debug)?;
275 require(
276 matches!(snapshot, WatchEvent::Snapshot { ref entries, .. } if entries.len() == 1),
277 "watch did not deliver the complete initial snapshot",
278 )?;
279 require(
280 watch.has_snapshot(),
281 "watch did not record snapshot receipt",
282 )?;
283 bus.put("module.a", b"v2".to_vec()).await.map_err(debug)?;
284 require(
285 matches!(
286 watch.next().await.map_err(debug)?,
287 WatchEvent::Update(RegisterUpdate::Put(_))
288 ),
289 "watch did not distinguish an update",
290 )
291}
292
293async fn work_redelivery() -> Result<(), String> {
294 let bus = queue_bus(3, 4_096);
295 let receipt = bus
296 .publish(
297 "ck.box.effect.a.s.intent",
298 "m1",
299 digest("m1"),
300 Headers::new(),
301 )
302 .await
303 .map_err(debug)?;
304 require(!receipt.duplicate(), "unexpected duplicate publish")?;
305 let first = item(bus.claim().await.map_err(debug)?)?;
306 bus.nak(first.token, Duration::ZERO).await.map_err(debug)?;
307 let second = item(bus.claim().await.map_err(debug)?)?;
308 require(
309 first.delivery_count == 1,
310 "first delivery count was not one",
311 )?;
312 require(second.delivery_count == 2, "redelivery count was not two")
313}
314
315async fn cap_exhaustion() -> Result<(), String> {
316 let bus = queue_bus(1, 4_096);
317 let receipt = bus
318 .publish(
319 "ck.box.effect.a.s.intent",
320 "m1",
321 digest("m1"),
322 Headers::new(),
323 )
324 .await
325 .map_err(debug)?;
326 require(!receipt.duplicate(), "unexpected duplicate publish")?;
327 let first = item(bus.claim().await.map_err(debug)?)?;
328 bus.nak(first.token, Duration::ZERO).await.map_err(debug)?;
329 let exhausted = match bus.claim().await.map_err(debug)? {
330 ClaimOutcome::MaxDeliveriesExceeded(exhausted) => exhausted,
331 other => {
332 return Err(format!(
333 "cap did not report MaxDeliveriesExceeded: {other:?}"
334 ))
335 }
336 };
337 let dead_stream = InMemoryBus::new(InMemoryConfig::default());
338 let queue = DeadLetterObservedQueue {
339 queue: &bus,
340 dead_stream: &dead_stream,
341 };
342 let receipt = terminally_dispose(
343 &dead_stream,
344 &queue,
345 "box",
346 &exhausted,
347 digest("dead-record"),
348 )
349 .await
350 .map_err(debug)?;
351 require(!receipt.duplicate(), "dead letter was not stored")?;
352 require(
353 bus.events()
354 .iter()
355 .any(|event| matches!(event, BackendEvent::Terminated { .. })),
356 "exhausted delivery was not terminated",
357 )
358}
359
360struct DeadLetterObservedQueue<'a> {
363 queue: &'a InMemoryBus,
364 dead_stream: &'a InMemoryBus,
365}
366
367#[async_trait]
368impl WorkQueue for DeadLetterObservedQueue<'_> {
369 async fn claim(&self) -> cortexkit_bus_trait::BusResult<ClaimOutcome> {
370 self.queue.claim().await
371 }
372
373 async fn ack(
374 &self,
375 token: cortexkit_bus_trait::DeliveryToken,
376 ) -> cortexkit_bus_trait::BusResult<()> {
377 self.queue.ack(token).await
378 }
379
380 async fn nak(
381 &self,
382 token: cortexkit_bus_trait::DeliveryToken,
383 delay: Duration,
384 ) -> cortexkit_bus_trait::BusResult<()> {
385 self.queue.nak(token, delay).await
386 }
387
388 async fn term(
389 &self,
390 token: cortexkit_bus_trait::DeliveryToken,
391 ) -> cortexkit_bus_trait::BusResult<()> {
392 if !self.dead_stream.events().iter().any(|event| {
393 matches!(event, BackendEvent::Published { subject, .. } if subject == "ck.box.effect.dead")
394 }) {
395 return Err(BusError::denied("ck.box.effect.dead", "dead-letter publish did not precede term"));
396 }
397 self.queue.term(token).await
398 }
399}
400
401async fn full_queue() -> Result<(), String> {
402 let bus = queue_bus(5, 150);
403 let receipt = bus
404 .publish(
405 "ck.box.effect.a.s.intent",
406 "first",
407 digest("first"),
408 Headers::new(),
409 )
410 .await
411 .map_err(debug)?;
412 require(!receipt.duplicate(), "unexpected duplicate publish")?;
413 let error = bus
414 .publish(
415 "ck.box.effect.a.s.intent",
416 "second",
417 digest("second"),
418 Headers::new(),
419 )
420 .await
421 .expect_err("second publish should exceed max-bytes");
422 require(
423 matches!(error, BusError::Unavailable { exhausted_limit: Some(ref limit), .. } if limit == "max_bytes"),
424 "full queue did not name max_bytes in Unavailable",
425 )
426}
427
428async fn error_taxonomy() -> Result<(), String> {
429 let denied_subject = "ck.box.room.denied.post";
430 let bus = InMemoryBus::new(
431 InMemoryConfig::default()
432 .deny_subject(denied_subject)
433 .with_work_queue(WorkQueueConfig {
434 subject: "ck.box.effect.a.s.intent".into(),
435 max_bytes: 4_096,
436 max_deliveries: 5,
437 max_nak_delay: Duration::from_millis(1),
438 }),
439 );
440 require(
441 matches!(bus.publish(denied_subject, "m", digest("m"), Headers::new()).await, Err(BusError::Denied { subject, .. }) if subject == denied_subject),
442 "fixture denial was not Denied",
443 )?;
444 require(
445 matches!(bus.get("missing").await, Err(BusError::Absent { .. })),
446 "missing register key was not Absent",
447 )?;
448 let receipt = bus
449 .publish(
450 "ck.box.effect.a.s.intent",
451 "work",
452 digest("work"),
453 Headers::new(),
454 )
455 .await
456 .map_err(debug)?;
457 require(!receipt.duplicate(), "unexpected duplicate publish")?;
458 let claimed = item(bus.claim().await.map_err(debug)?)?;
459 require(
460 matches!(
461 bus.nak(claimed.token, Duration::from_secs(1)).await,
462 Err(BusError::Clamped {
463 retry_after: Some(_),
464 ..
465 })
466 ),
467 "oversized delay was not reported Clamped",
468 )?;
469 require(
470 matches!(
471 map_connection_failure(ConnectionFailure::WriteOnSeveredSocket),
472 BusError::Unavailable { .. }
473 ),
474 "severed write was not Unavailable",
475 )?;
476 require(
477 matches!(
478 map_connection_failure(ConnectionFailure::ServerClosedThenReauthenticationRefused {
479 subject: denied_subject.into(),
480 reason: "revoked".into()
481 }),
482 BusError::Denied { .. }
483 ),
484 "reconnect refusal was not Denied",
485 )?;
486 require(
487 matches!(
488 map_credential_sign_failure(CredentialSignFailure::RecordMissing {
489 record: "nats.module.g1.e0".into()
490 }),
491 BusError::Absent { .. }
492 ),
493 "missing signing record was not Absent",
494 )
495}
496
497fn queue_bus(max_deliveries: u32, max_bytes: usize) -> InMemoryBus {
498 InMemoryBus::new(InMemoryConfig::default().with_work_queue(WorkQueueConfig {
499 subject: "ck.box.effect.a.s.intent".into(),
500 max_bytes,
501 max_deliveries,
502 max_nak_delay: Duration::from_secs(30),
503 }))
504}
505
506fn item(outcome: ClaimOutcome) -> Result<cortexkit_bus_trait::WorkItem, String> {
507 match outcome {
508 ClaimOutcome::Item(item) => Ok(item),
509 other => Err(format!("expected work item, got {other:?}")),
510 }
511}
512
513fn digest(value: &str) -> ContentDigest {
514 ContentDigest::of_bytes(value.as_bytes())
515}
516
517fn require(condition: bool, reason: &str) -> Result<(), String> {
518 condition.then_some(()).ok_or_else(|| reason.into())
519}
520
521fn debug(error: impl std::fmt::Debug) -> String {
522 format!("{error:?}")
523}
524
525#[cfg(test)]
526mod tests {
527 use super::*;
528 use cortexkit_bus_trait::{
529 classify_properties, run_conformance, PropertyApplicability, PropertySet, SuiteBug,
530 CONFORMANCE_PROPERTIES,
531 };
532
533 struct WeakenedNatsFixture;
534
535 #[async_trait]
536 impl ConformanceBackend for WeakenedNatsFixture {
537 fn backend_kind(&self) -> BackendKind {
538 BackendKind::Nats
539 }
540
541 async fn probe(&self, property: PropertySpec) -> PropertyOutcome {
542 match property.set {
543 PropertySet::Parity => InMemoryConformance.probe(property).await,
544 PropertySet::NatsOnly => PropertyOutcome::Failed {
545 reason: "fixture deliberately has only process-local semantics".into(),
546 },
547 }
548 }
549 }
550
551 #[tokio::test]
552 async fn in_memory_passes_every_parity_property() {
553 let report = run_conformance(&InMemoryConformance, PropertySet::Parity)
554 .await
555 .expect("parity is applicable to in-memory");
556
557 assert_eq!(report.property_set_name, "parity");
558 for (property, outcome) in &report.outcomes {
559 assert_eq!(outcome, &PropertyOutcome::Passed, "{}", property.name);
560 }
561 assert!(report.passed());
562 }
563
564 #[tokio::test]
565 async fn asserting_nats_only_against_in_memory_is_a_suite_bug() {
566 let result = run_conformance(&InMemoryConformance, PropertySet::NatsOnly).await;
567 assert_eq!(result, Err(SuiteBug::NatsOnlyAssertedAgainstInMemory));
568 }
569
570 #[tokio::test]
571 async fn weakened_backend_passes_real_parity_probes_but_fails_nats_only() {
572 let parity = run_conformance(&WeakenedNatsFixture, PropertySet::Parity)
573 .await
574 .expect("parity run is valid");
575 let nats_only = run_conformance(&WeakenedNatsFixture, PropertySet::NatsOnly)
576 .await
577 .expect("NATS-only run is valid for a NATS-labelled fixture");
578
579 assert!(parity.passed());
580 assert!(!nats_only.passed());
581 }
582
583 #[test]
584 fn every_nats_only_property_is_not_applicable_not_passed() {
585 let classifications = classify_properties(BackendKind::InMemory);
586 let nats_only = classifications
587 .iter()
588 .filter(|row| row.property.set == PropertySet::NatsOnly)
589 .collect::<Vec<_>>();
590
591 assert_eq!(
592 nats_only.len(),
593 CONFORMANCE_PROPERTIES
594 .iter()
595 .filter(|property| property.set == PropertySet::NatsOnly)
596 .count()
597 );
598 assert!(nats_only.iter().all(|row| matches!(
599 row.applicability,
600 PropertyApplicability::NotApplicable { .. }
601 )));
602 }
603}