Chat RUDP test: an open the mouth refuses as UnknownKind ends the wait and is heard (failing: the refusal is ignored); MouthRefused error type

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Eureka 2026-11-10 23:44:30 +00:00
commit e43a7343f8
2 changed files with 49 additions and 5 deletions

View file

@ -151,6 +151,19 @@ impl<'de> Deserialize<'de> for Refusal {
}
}
/// The mouth refused a request outright with a typed refusal frame, and nothing was done. An error of `open_direct` when the
/// mouth does not serve the kind: told as not sent, unlike a silent mouth, whose open may have gone out.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct MouthRefused(pub Refusal);
impl std::fmt::Display for MouthRefused {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "the mouth refused the request: {}", self.0.name())
}
}
impl std::error::Error for MouthRefused {}
/// The mouth's typed answer to a request that names a Persona it does not
/// serve: the pending request, the ack and the send alike.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]

View file

@ -13,7 +13,7 @@ use std::thread::JoinHandle;
use std::time::Duration;
use cultnet_rs::{CultNetMessage, CultNetRawDocumentRecord, CultNetRawPayloadEncoding, CultNetRudpSocketTransportConnection, CultNetRudpSocketTransportOptions};
use mind_body::chat_client::{self, ACK, Chat, ChatAck, ChatOpenDirectReceipt, ChatOpenDirectRequest, DirectOutcome, OPEN_DIRECT_RECEIPT, OPEN_DIRECT_REQUEST, ChatPending, ChatPendingRequest, ChatRead, ChatSendReceipt, ChatSendRequest, PENDING, PENDING_REQUEST, PENDING_V1, READ, SEND_RECEIPT, SEND_REQUEST};
use mind_body::chat_client::{self, MouthRefused, Refusal, REFUSAL, ACK, Chat, ChatAck, ChatOpenDirectReceipt, ChatOpenDirectRequest, DirectOutcome, OPEN_DIRECT_RECEIPT, OPEN_DIRECT_REQUEST, ChatPending, ChatPendingRequest, ChatRead, ChatSendReceipt, ChatSendRequest, PENDING, PENDING_REQUEST, PENDING_V1, READ, SEND_RECEIPT, SEND_REQUEST};
use mind_body::chat_rudp::{RudpChat, connection_id};
const ACK_BYTES: &[u8] = include_bytes!("fixtures/persona-chat-v1/gamecult.persona.chat_ack.v1.msgpack");
@ -23,11 +23,15 @@ const PENDING_REQUEST_BYTES: &[u8] = include_bytes!("fixtures/persona-chat-v1/ga
const SEND_RECEIPT_BYTES: &[u8] = include_bytes!("fixtures/persona-chat-v2/gamecult.persona.chat_send_receipt.v2.sent.msgpack");
const SEND_REQUEST_BYTES: &[u8] = include_bytes!("fixtures/persona-chat-v2/gamecult.persona.chat_send_request.v2.nil-basis.msgpack");
const OPEN_REQUEST_BYTES: &[u8] = include_bytes!("fixtures/persona-chat-v4/gamecult.persona.chat_open_direct_request.v1.msgpack");
const UNKNOWN_KIND_BYTES: &[u8] = include_bytes!("fixtures/persona-chat-v4/gamecult.persona.chat_refusal.v1.unknown-kind.msgpack");
const OPENED_BYTES: &[u8] = include_bytes!("fixtures/persona-chat-v4/gamecult.persona.chat_open_direct_receipt.v1.opened.msgpack");
const KEY: &str = "connection-key-for-test";
const TAG: &str = "cultnet.transport.rudp.v0";
/// What a fixture mouth answers an open with: the document kind, a record key (none: the request's own), and its bytes.
type OpenReply = (&'static str, Option<&'static str>, &'static [u8]);
type Seen = Arc<Mutex<Vec<(String, Vec<u8>)>>>;
struct Mouth {
@ -70,15 +74,15 @@ fn mouth(key: &str) -> Mouth {
/// A mouth that has no image field yet: it answers a pending request in the v1 kind.
fn legacy_mouth(key: &str) -> Mouth {
serve(key, "127.0.0.1:0", PENDING_V1, PENDING_V1_BYTES)
serve(key, "127.0.0.1:0", PENDING_V1, PENDING_V1_BYTES, (OPEN_DIRECT_RECEIPT, None, OPENED_BYTES))
}
/// The same mouth on a chosen address: a restart binds the address the old one held.
fn mouth_at(key: &str, bind: &str) -> Mouth {
serve(key, bind, PENDING, PENDING_BYTES)
serve(key, bind, PENDING, PENDING_BYTES, (OPEN_DIRECT_RECEIPT, None, OPENED_BYTES))
}
fn serve(key: &str, bind: &str, pending_kind: &'static str, pending_bytes: &'static [u8]) -> Mouth {
fn serve(key: &str, bind: &str, pending_kind: &'static str, pending_bytes: &'static [u8], open: OpenReply) -> Mouth {
let socket = UdpSocket::bind(bind).unwrap();
socket.set_read_timeout(Some(Duration::from_millis(20))).unwrap();
let address = socket.local_addr().unwrap();
@ -98,7 +102,7 @@ fn serve(key: &str, bind: &str, pending_kind: &'static str, pending_bytes: &'sta
let reply = match document.schema_id.as_str() {
PENDING_REQUEST if for_eureka(&chat_client::decode::<ChatPendingRequest>(&document.payload).unwrap().persona) => Some(answer(pending_kind, "eureka", pending_bytes)),
SEND_REQUEST if for_eureka(&chat_client::decode::<ChatSendRequest>(&document.payload).unwrap().persona) => Some(answer(SEND_RECEIPT, &document.record_key, SEND_RECEIPT_BYTES)),
OPEN_DIRECT_REQUEST if for_eureka(&chat_client::decode::<ChatOpenDirectRequest>(&document.payload).unwrap().persona) => Some(answer(OPEN_DIRECT_RECEIPT, &document.record_key, OPENED_BYTES)),
OPEN_DIRECT_REQUEST if for_eureka(&chat_client::decode::<ChatOpenDirectRequest>(&document.payload).unwrap().persona) => Some(answer(open.0, open.1.unwrap_or(&document.record_key), open.2)),
_ => None,
};
if let Some(reply) = reply {
@ -231,3 +235,30 @@ fn an_open_crosses_a_real_rudp_session_in_the_mouths_bytes_and_a_receipt_for_ano
let seen = mouth.seen.lock().unwrap().clone();
assert_eq!(seen[0], (OPEN_DIRECT_REQUEST.to_string(), OPEN_REQUEST_BYTES.to_vec()));
}
/// A mouth that answers every open with `reply`.
fn opening_with(reply: OpenReply) -> Mouth {
serve(KEY, "127.0.0.1:0", PENDING, PENDING_BYTES, reply)
}
#[test]
fn a_mouth_that_does_not_serve_the_open_kind_refuses_it_and_the_open_is_told_not_sent_at_once() {
let mouth = opening_with((REFUSAL, Some("eureka"), UNKNOWN_KIND_BYTES));
let chat = client(&mouth, KEY, "eureka", Duration::from_secs(20));
let request = chat_client::decode::<ChatOpenDirectRequest>(OPEN_REQUEST_BYTES).unwrap();
let started = std::time::Instant::now();
let error = chat.open_direct(&request).unwrap_err();
assert!(started.elapsed() < Duration::from_secs(5), "the refusal ends the wait: {:?}", started.elapsed());
assert_eq!(error.downcast_ref::<MouthRefused>(), Some(&MouthRefused(Refusal::UnknownKind)), "{error}");
}
#[test]
fn a_refusal_for_another_persona_or_another_kind_does_not_satisfy_the_open() {
let wrong_persona = opening_with((REFUSAL, Some("someone-else"), UNKNOWN_KIND_BYTES));
let request = chat_client::decode::<ChatOpenDirectRequest>(OPEN_REQUEST_BYTES).unwrap();
let error = client(&wrong_persona, KEY, "eureka", Duration::from_millis(800)).open_direct(&request).unwrap_err();
assert!(error.downcast_ref::<MouthRefused>().is_none(), "a refusal keyed by another persona is not heard: {error}");
let wrong_kind = opening_with((SEND_RECEIPT, Some("eureka"), SEND_RECEIPT_BYTES));
let error = client(&wrong_kind, KEY, "eureka", Duration::from_millis(800)).open_direct(&request).unwrap_err();
assert!(error.downcast_ref::<MouthRefused>().is_none(), "another kind is not heard: {error}");
}