#![allow(
clippy::expect_used,
clippy::unwrap_used,
clippy::panic,
clippy::items_after_statements
)]
use std::time::{Duration, Instant};
use liminal::protocol::Frame;
use super::{DEFAULT_REPLY_TIMEOUT, PendingReplyTable, test_reply_envelope};
fn table() -> PendingReplyTable {
PendingReplyTable::new(8, 32, DEFAULT_REPLY_TIMEOUT)
}
fn now() -> Instant {
Instant::now()
}
fn later(base: Instant) -> Instant {
base + DEFAULT_REPLY_TIMEOUT + Duration::from_secs(1)
}
#[test]
fn multiple_pipelined_replies_on_one_conversation_match_fifo() {
let mut t = table();
let base = now();
t.admit(1, 10, base).expect("admit 1");
t.admit(1, 11, base).expect("admit 2");
t.admit(1, 12, base).expect("admit 3");
assert_eq!(t.pending_for(1), 3);
for stream in [10_u32, 11, 12] {
let frame = t
.match_reply(1, test_reply_envelope(b"r"))
.expect("a pending entry matches");
assert!(
matches!(frame, Frame::ConversationMessage { stream_id, conversation_id: 1, .. } if stream_id == stream),
"FIFO: reply matches the oldest pending entry (stream {stream})"
);
}
assert_eq!(t.len(), 0, "all entries consumed");
}
#[test]
fn replies_are_correlated_per_conversation() {
let mut t = table();
let base = now();
t.admit(1, 10, base).expect("admit c1");
t.admit(2, 20, base).expect("admit c2");
let frame = t
.match_reply(2, test_reply_envelope(b"r"))
.expect("c2 match");
assert!(matches!(
frame,
Frame::ConversationMessage {
stream_id: 20,
conversation_id: 2,
..
}
));
assert_eq!(t.pending_for(1), 1, "conversation 1's entry is untouched");
}
#[test]
fn timeout_then_late_reply_then_new_request() {
let mut t = table();
let base = now();
t.admit(1, 10, base).expect("admit");
let expired = t.expire_due(later(base));
assert_eq!(expired.len(), 1, "one timeout frame");
assert!(matches!(
expired[0],
Frame::ConversationError {
stream_id: 10,
conversation_id: 1,
..
}
));
assert_eq!(t.tombstones_for(1), 1);
assert_eq!(t.pending_for(1), 0);
assert!(
t.match_reply(1, test_reply_envelope(b"late")).is_none(),
"a late reply is discarded, not delivered"
);
assert_eq!(
t.len(),
0,
"the tombstone is freed by the late-reply consume"
);
t.admit(1, 11, base).expect("new admit after recovery");
let frame = t
.match_reply(1, test_reply_envelope(b"fresh"))
.expect("match");
assert!(matches!(
frame,
Frame::ConversationMessage { stream_id: 11, .. }
));
}
#[test]
fn capacity_recovers_via_late_reply_consume() {
let mut t = table();
let base = now();
t.admit(1, 10, base).expect("admit");
t.expire_due(later(base)); assert_eq!(t.tombstones_for(1), 1);
assert!(t.match_reply(1, test_reply_envelope(b"late")).is_none());
assert_eq!(t.len(), 0, "late-reply consume frees the slot");
}
#[test]
fn capacity_recovers_via_conversation_close() {
let mut t = table();
let base = now();
t.admit(1, 10, base).expect("admit");
t.expire_due(later(base)); t.admit(1, 11, base).expect("second admit under sub-cap");
assert_eq!(t.len(), 2);
t.remove_conversation(1);
assert_eq!(
t.len(),
0,
"close sweep clears every entry for the conversation"
);
}
#[test]
fn wedged_conversation_refuses_while_siblings_proceed() {
let mut t = PendingReplyTable::new(2, 32, DEFAULT_REPLY_TIMEOUT);
let base = now();
t.admit(1, 10, base).expect("c1 admit 1");
t.admit(1, 11, base).expect("c1 admit 2");
t.expire_due(later(base));
assert_eq!(
t.tombstones_for(1),
2,
"conversation 1 is full of tombstones"
);
let refusal = t
.admit(1, 12, base)
.expect_err("wedged conversation refuses");
assert!(
matches!(
refusal,
crate::ServerError::ConnectionCapReached {
cap: "max_pending_replies_per_conversation",
..
}
),
"the self-wedge is the typed per-conversation cap refusal"
);
t.admit(2, 20, base).expect("sibling conversation proceeds");
let frame = t
.match_reply(2, test_reply_envelope(b"r"))
.expect("sibling match");
assert!(matches!(
frame,
Frame::ConversationMessage {
conversation_id: 2,
..
}
));
}
#[test]
fn slow_actor_late_reply_never_matches_a_younger_entry() {
let mut t = table();
let base = now();
t.admit(1, 10, base).expect("admit old");
t.expire_due(later(base));
assert_eq!(t.tombstones_for(1), 1);
t.admit(1, 11, base).expect("admit young");
assert_eq!(t.pending_for(1), 1);
assert!(
t.match_reply(1, test_reply_envelope(b"very-late"))
.is_none(),
"the very-late reply is discarded via the tombstone, not delivered"
);
assert_eq!(
t.pending_for(1),
1,
"the younger entry is still pending — never mis-matched to the old reply"
);
assert_eq!(t.tombstones_for(1), 0, "the tombstone was consumed");
let frame = t
.match_reply(1, test_reply_envelope(b"young"))
.expect("young match");
assert!(matches!(
frame,
Frame::ConversationMessage { stream_id: 11, .. }
));
}
#[test]
fn connection_cap_counts_pending_only_not_tombstones() {
let mut t = PendingReplyTable::new(64, 2, DEFAULT_REPLY_TIMEOUT);
let base = now();
t.admit(1, 10, base).expect("admit 1");
t.admit(2, 20, base).expect("admit 2"); let refusal = t.admit(3, 30, base).expect_err("connection table full");
assert!(matches!(
refusal,
crate::ServerError::ConnectionCapReached {
cap: "max_pending_conversation_replies_per_connection",
..
}
));
t.expire_due(later(base));
t.admit(3, 30, base)
.expect("tombstones free the connection table for new pending entries");
}
#[test]
fn expire_is_idempotent_across_slices() {
let mut t = table();
let base = now();
t.admit(1, 10, base).expect("admit");
let first = t.expire_due(later(base));
assert_eq!(
first.len(),
1,
"the entry tombstones once, emitting one frame"
);
let second = t.expire_due(later(base));
assert!(
second.is_empty(),
"an already-tombstoned entry never re-times-out"
);
}
#[test]
fn conversations_awaiting_reply_lists_only_pending_conversations() {
let mut t = table();
let base = now();
t.admit(1, 10, base).expect("admit c1");
t.admit(2, 20, base).expect("admit c2");
t.expire_due(later(base)); assert!(
t.conversations_awaiting_reply().is_empty(),
"a conversation with only tombstones is not awaiting a (matchable) reply"
);
t.admit(3, 30, base).expect("admit c3 pending");
assert_eq!(t.conversations_awaiting_reply(), vec![3]);
}
#[test]
fn cancel_all_drops_every_entry() {
let mut t = table();
let base = now();
t.admit(1, 10, base).expect("admit");
t.admit(2, 20, base).expect("admit");
t.expire_due(later(base));
t.cancel_all();
assert_eq!(t.len(), 0, "finalization cancels every entry");
}