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