diff --git a/crates/wasm-actor/tests/wasm_actor.rs b/crates/wasm-actor/tests/wasm_actor.rs index b1ffd60..1c6a13f 100644 --- a/crates/wasm-actor/tests/wasm_actor.rs +++ b/crates/wasm-actor/tests/wasm_actor.rs @@ -9487,4 +9487,139 @@ proptest! { let resp = inbox.try_recv().expect("echo should respond"); assert_eq!(resp.0, payload, "echo must preserve content exactly"); } +} + +// ── Cycle 65 ───────────────────────────────────────────────────────────────── + +// Guest that traps in alloc (not handle) — message dropped, actor survives +#[test] +fn alloc_trap_recovery_then_normal_message() { + // First message: alloc traps. Second message: alloc works, handle echoes. + let wat = r#"(module + (import "swactor" "send" (func $send (param i32 i32 i32))) + (memory (export "memory") 1) + (global $call_count (mut i32) (i32.const 0)) + (func (export "alloc") (param $len i32) (result i32) + ;; First call: trap + (if (i32.eqz (global.get $call_count)) + (then + (global.set $call_count (i32.const 1)) + unreachable + ) + ) + ;; Subsequent calls: return fixed ptr + i32.const 1024 + ) + (func (export "handle") (param $ptr i32) (param $len i32) + ;; Echo: first 32 bytes dest, rest payload + (if (i32.ge_u (local.get $len) (i32.const 33)) + (then + (call $send + (local.get $ptr) + (i32.add (local.get $ptr) (i32.const 32)) + (i32.sub (local.get $len) (i32.const 32)) + ) + ) + ) + ) + )"#; + let engine = SharedEngine::new().unwrap(); + let actor = WasmActorBuilder::new(engine, wat::parse_str(wat).unwrap()) + .build().unwrap(); + let rt = Runtime::new(RuntimeConfig::default()); + let inbox = rt.new_inbox::().unwrap(); + let addr = rt.spawn(actor).unwrap(); + + // First message — alloc traps, dropped + rt.send_to(addr, framed_msg(inbox.addr(), b"trap")).unwrap(); + rt.tick(); + assert!(inbox.try_recv().is_none(), "first message dropped due to alloc trap"); + + // Second message — should work + rt.send_to(addr, framed_msg(inbox.addr(), b"ok")).unwrap(); + rt.tick(); + let resp = inbox.try_recv().expect("second message should echo"); + assert_eq!(resp.0, b"ok"); +} + +// Guest module with table but no call_indirect — table exists but unused +#[test] +fn module_with_unused_table() { + let wat = r#"(module + (memory (export "memory") 1) + (table 2 funcref) + (func (export "alloc") (param $len i32) (result i32) i32.const 1024) + (func (export "handle") (param $ptr i32) (param $len i32)) + )"#; + let engine = SharedEngine::new().unwrap(); + let actor = WasmActorBuilder::new(engine, wat::parse_str(wat).unwrap()) + .build().unwrap(); + let rt = Runtime::new(RuntimeConfig::default()); + let addr = rt.spawn(actor).unwrap(); + rt.send_to(addr, ByteMessage(b"table".to_vec())).unwrap(); + rt.tick(); +} + +// Echo actor processes alternating large and small messages +#[test] +fn alternating_large_small_messages() { + let engine = SharedEngine::new().unwrap(); + let actor = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); + let rt = Runtime::new(RuntimeConfig::default()); + let inbox = rt.new_inbox::().unwrap(); + let addr = rt.spawn(actor).unwrap(); + + for i in 0..10 { + let size = if i % 2 == 0 { 5000 } else { 3 }; + let payload = vec![(i as u8).wrapping_mul(7); size]; + rt.send_to(addr, framed_msg(inbox.addr(), &payload)).unwrap(); + } + rt.tick(); + + let msgs: Vec> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); + assert_eq!(msgs.len(), 10); + for (i, msg) in msgs.iter().enumerate() { + let expected_size = if i % 2 == 0 { 5000 } else { 3 }; + assert_eq!(msg.len(), expected_size, "message {i} wrong size"); + assert!(msg.iter().all(|&b| b == (i as u8).wrapping_mul(7)), + "message {i} wrong content"); + } +} + +// Send 200 messages to silent actor — no responses, no panics +#[test] +fn silent_actor_absorbs_200_messages() { + let engine = SharedEngine::new().unwrap(); + let actor = WasmActorBuilder::new(engine, guest_wasm("silent")).build().unwrap(); + let rt = Runtime::new(RuntimeConfig::default()); + let addr = rt.spawn(actor).unwrap(); + + for _ in 0..200 { + rt.send_to(addr, ByteMessage(vec![0xFF; 50])).unwrap(); + } + for _ in 0..10 { + rt.tick(); + } +} + +// Build two actors from same bytes object (cloned), verify independence +#[test] +fn two_actors_from_cloned_wasm_bytes() { + let engine = SharedEngine::new().unwrap(); + let bytes = guest_wasm("echo"); + let a1 = WasmActorBuilder::new(engine.clone(), bytes.clone()).build().unwrap(); + let a2 = WasmActorBuilder::new(engine, bytes).build().unwrap(); + + let rt = Runtime::new(RuntimeConfig::default()); + let inbox1 = rt.new_inbox::().unwrap(); + let inbox2 = rt.new_inbox::().unwrap(); + let addr1 = rt.spawn(a1).unwrap(); + let addr2 = rt.spawn(a2).unwrap(); + + rt.send_to(addr1, framed_msg(inbox1.addr(), b"one")).unwrap(); + rt.send_to(addr2, framed_msg(inbox2.addr(), b"two")).unwrap(); + rt.tick(); + + assert_eq!(inbox1.try_recv().unwrap().0, b"one"); + assert_eq!(inbox2.try_recv().unwrap().0, b"two"); } \ No newline at end of file