diff --git a/Cargo.lock b/Cargo.lock index 945a03c..cedde91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2747,6 +2747,7 @@ dependencies = [ name = "swactor-wasm-actor" version = "0.1.0" dependencies = [ + "proptest", "swactor", "wasmtime", "wat", diff --git a/crates/wasm-actor/tests/wasm_actor.rs b/crates/wasm-actor/tests/wasm_actor.rs index 05e86a4..14f3b36 100644 --- a/crates/wasm-actor/tests/wasm_actor.rs +++ b/crates/wasm-actor/tests/wasm_actor.rs @@ -1202,6 +1202,79 @@ fn native_handler_spawns_wasm_actor_and_forwards_message() { assert_eq!(received.0, b"spawned-echo"); } +// ── Bounded mailbox: WASM actor with backpressure ──────────────────────────── + +#[test] +fn bounded_mailbox_applies_to_wasm_actor() { + // With a bounded mailbox of capacity 3, sending 10 messages should + // result in only 3 being processed (DropNewest policy). + use swactor::runtime::MailboxOverflow; + + let engine = SharedEngine::new().unwrap(); + let actor = WasmActorBuilder::new(engine, guest_wasm("echo")) + .build() + .unwrap(); + + let config = RuntimeConfig { + default_mailbox_capacity: 3, + mailbox_overflow: MailboxOverflow::DropNewest, + ..RuntimeConfig::default() + }; + let rt = Runtime::new(config); + let inbox = rt.new_inbox::().unwrap(); + let addr = rt.spawn(actor).unwrap(); + + // Send 10 messages before any tick — only first 3 should be kept + for i in 0u8..10 { + rt.send_to(addr, framed_msg(inbox.addr(), &[i])).unwrap(); + } + rt.tick(); + + let mut received = Vec::new(); + while let Some(msg) = inbox.try_recv() { + received.push(msg.0[0]); + } + + assert_eq!(received.len(), 3, "bounded mailbox should limit to 3 messages"); + // DropNewest keeps the first 3 sent + assert_eq!(received, vec![0, 1, 2]); +} + +// ── Property: alloc failures never kill the actor ──────────────────────────── + +proptest! { + #[test] + fn prop_any_alloc_return_value_never_kills_actor(alloc_val in -100i32..70000) { + // Regardless of what alloc returns (negative, zero, OOB, valid), + // sending a message should never kill the actor. + let alloc_const = format!("i32.const {alloc_val}"); + let wat = format!(r#" + (module + (import "swactor" "send" (func $send (param i32 i32 i32))) + (memory (export "memory") 1) + (func (export "alloc") (param i32) (result i32) + {alloc_const} + ) + (func (export "handle") (param i32 i32)) + ) + "#); + let wasm = wat::parse_str(&wat).unwrap(); + let engine = SharedEngine::new().unwrap(); + let actor = WasmActorBuilder::new(engine, wasm).build().unwrap(); + + let rt = Runtime::new(RuntimeConfig::default()); + let addr = rt.spawn(actor).unwrap(); + + // Send a message — should never panic or poison + rt.send_to(addr, ByteMessage(vec![0u8; 100])).unwrap(); + rt.tick(); + + // Actor should still accept messages (not poisoned) + let result = rt.send_to(addr, ByteMessage(vec![1])); + prop_assert!(result.is_ok(), "actor should survive any alloc return value: {alloc_val}"); + } +} + // ── Multi-worker: WASM actors across threads ───────────────────────────────── #[test]