//! Invariants for the `EventEnvelope` constructors and `CallError` //! payloads (ADR-014/ADR-016): structural round-trip through //! `write_frame`/`read_frame` for every event kind, `CallError` //! serialization never panics and parses back, and the wire event-type //! constants survive the constructors. use crate::arbitrary_value::JsonValue; use alkcall::protocol::wire::{ CallError, EventEnvelope, FrameError, FrameFramedReader, FrameFramedWriter, }; use std::io::Cursor; #[derive(Debug, arbitrary::Arbitrary)] pub enum EnvelopeKind { Requested { id_len: u8, id_byte: u8, payload: JsonValue, }, Responded { id_len: u8, id_byte: u8, output: JsonValue, }, Completed { id_len: u8, id_byte: u8, }, Published { id_len: u8, id_byte: u8, chunk: JsonValue, }, Aborted { id_len: u8, id_byte: u8, }, Error { id_len: u8, id_byte: u8, code_len: u8, code_byte: u8, message_len: u8, message_byte: u8, retryable: bool, details: Option, }, } fn repeated_string(len: u8, byte: u8) -> String { // Bounded: at most 64 repetitions of one char. Long strings are a // frame-size concern covered by `envelope_frame`; this target is // about the constructors and structural invariants. let n = (len % 64) as usize; std::iter::repeat_n(byte as char, n).collect() } fn assert_round_trip(envelope: &EventEnvelope) { let mut bytes = Vec::new(); { let mut writer = FrameFramedWriter::new(&mut bytes); tokio::runtime::Builder::new_current_thread() .build() .expect("runtime builds") .block_on(writer.write_frame(envelope)) .expect("write_frame of any constructor output must succeed"); } let mut reader = FrameFramedReader::new(Cursor::new(bytes)); let decoded = match tokio::runtime::Builder::new_current_thread() .build() .expect("runtime builds") .block_on(reader.read_frame()) { Ok(decoded) => decoded, Err(FrameError::Json(e)) => { panic!("a written envelope must re-decode: {e}"); } Err(other) => panic!("a written envelope must re-decode, got {other:?}"), }; assert_eq!( &decoded, envelope, "structural round-trip (serde_json key order is not preserved)" ); } pub fn fuzz_envelope_semantic(kind: &EnvelopeKind) { let envelope = match kind { EnvelopeKind::Requested { id_len, id_byte, payload, } => EventEnvelope::requested(repeated_string(*id_len, *id_byte), payload.0.clone()), EnvelopeKind::Responded { id_len, id_byte, output, } => EventEnvelope::responded(repeated_string(*id_len, *id_byte), output.0.clone()), EnvelopeKind::Completed { id_len, id_byte } => { EventEnvelope::completed(repeated_string(*id_len, *id_byte)) } EnvelopeKind::Published { id_len, id_byte, chunk, } => EventEnvelope::published(repeated_string(*id_len, *id_byte), chunk.0.clone()), EnvelopeKind::Aborted { id_len, id_byte } => { EventEnvelope::aborted(repeated_string(*id_len, *id_byte)) } EnvelopeKind::Error { id_len, id_byte, code_len, code_byte, message_len, message_byte, retryable, details, } => { let mut error = CallError::new( repeated_string(*code_len, *code_byte), repeated_string(*message_len, *message_byte), *retryable, ); if let Some(details) = details { error = error.with_details(details.0.clone()); } EventEnvelope::error(repeated_string(*id_len, *id_byte), &error) } }; // The event-type constants are wire-stable; the constructors must // emit exactly them. let expected_type = match kind { EnvelopeKind::Requested { .. } => alkcall::protocol::wire::EVENT_REQUESTED, EnvelopeKind::Responded { .. } => alkcall::protocol::wire::EVENT_RESPONDED, EnvelopeKind::Completed { .. } => alkcall::protocol::wire::EVENT_COMPLETED, EnvelopeKind::Published { .. } => alkcall::protocol::wire::EVENT_PUBLISHED, EnvelopeKind::Aborted { .. } => alkcall::protocol::wire::EVENT_ABORTED, EnvelopeKind::Error { .. } => alkcall::protocol::wire::EVENT_ERROR, }; assert_eq!(envelope.r#type, expected_type); // The `payload` field is schema-free JSON: any Value must survive // a serde round-trip intact (the serde_json 128-depth recursion // limit is a decode-side guard, not an encode-side one). let as_value = serde_json::to_value(&envelope).expect("envelope serializes"); let reparsed: EventEnvelope = serde_json::from_value(as_value).expect("a serialized envelope re-parses"); assert_eq!( reparsed, envelope, "serde round-trip is structural identity" ); // A `call.error` payload always parses back into a `CallError` // (ADR-016: the error schema is closed and serde-derivable). if matches!(kind, EnvelopeKind::Error { .. }) { let parsed: Result = serde_json::from_value(envelope.payload.clone()); assert!( parsed.is_ok(), "call.error payload must parse back as CallError, got {:?}", envelope.payload ); } assert_round_trip(&envelope); }