diff --git a/crates/wasm-actor/tests/wasm_actor.rs b/crates/wasm-actor/tests/wasm_actor.rs index 125d80a..3a4735c 100644 --- a/crates/wasm-actor/tests/wasm_actor.rs +++ b/crates/wasm-actor/tests/wasm_actor.rs @@ -740,6 +740,191 @@ fn native_actor_communicates_with_wasm_actor() { assert_eq!(received.0, b"from native"); } +// ── Stale outbox: sends before trap leak into next handle ───────────────────── + +#[test] +#[ignore] // BUG: outbox not cleared on handle trap — stale entries leak into next call +fn outbox_entries_from_trapped_handle_do_not_leak_into_next_call() { + // A guest that calls swactor.send() successfully, then traps. + // The outbox contains the send from before the trap. + // On the next handle call (which succeeds without sending), the stale + // outbox entry should NOT be delivered. + // + // Counter incremented BEFORE the if-branch so it persists past the trap. + let wat = r#" + (module + (import "swactor" "send" (func $send (param i32 i32 i32))) + (memory (export "memory") 1) + (global $counter (mut i32) (i32.const 0)) + + (func (export "alloc") (param i32) (result i32) + i32.const 256 + ) + (func (export "handle") (param $ptr i32) (param $len i32) + ;; Increment counter first (survives trap) + global.get $counter + i32.const 1 + i32.add + global.set $counter + + ;; If counter was 0 (now 1): send then trap + global.get $counter + i32.const 1 + i32.eq + if + local.get $ptr ;; dest_ptr (first 32 bytes = inbox address) + i32.const 32 ;; payload_ptr + i32.const 1 ;; payload_len + call $send + unreachable ;; trap after send + end + ;; counter > 1: do nothing (no send) + ) + ) + "#; + 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 inbox = rt.new_inbox::().unwrap(); + let addr = rt.spawn(actor).unwrap(); + + // First message: guest sends to outbox then traps — stale entry in outbox + // Use framed_msg so the first 32 bytes are the inbox address + rt.send_to(addr, framed_msg(inbox.addr(), b"x")).unwrap(); + rt.tick(); + + // No message should have been delivered (handle trapped before outbox drain) + assert!(inbox.try_recv().is_none(), "trapped handle should not deliver messages"); + + // Second message: guest does nothing (counter=2, no send, no trap). + // If the outbox wasn't cleared, the stale entry would be drained here. + rt.send_to(addr, framed_msg(inbox.addr(), b"y")).unwrap(); + rt.tick(); + + // Should still be empty — the stale outbox entry must not leak + assert!( + inbox.try_recv().is_none(), + "stale outbox entry from trapped call should not leak into next handle" + ); +} + +// ── Builder validation: invalid WASM bytes ─────────────────────────────────── + +#[test] +fn invalid_wasm_bytes_returns_wasmtime_error() { + let garbage = vec![0u8, 1, 2, 3]; // not valid wasm + let engine = SharedEngine::new().unwrap(); + let result = WasmActorBuilder::new(engine, garbage).build(); + match result { + Err(WasmActorError::Wasmtime(_)) => {} // expected — compilation failure + Err(other) => panic!("expected Wasmtime error for invalid bytes, got: {other}"), + Ok(_) => panic!("should reject invalid wasm bytes"), + } +} + +// ── Guest sends zero-length payload ────────────────────────────────────────── + +#[test] +fn guest_send_with_zero_length_payload_delivers_empty_message() { + // Guest calls swactor.send with payload_len=0. This should produce + // a ByteMessage(vec![]) at the destination. + let wat = r#" + (module + (import "swactor" "send" (func $send (param i32 i32 i32))) + (memory (export "memory") 1) + (func (export "alloc") (param i32) (result i32) + i32.const 256 ;; valid allocation + ) + (func (export "handle") (param $ptr i32) (param $len i32) + ;; Send with the first 32 bytes as dest, zero-length payload + local.get $ptr + i32.const 32 ;; payload_ptr (doesn't matter, len is 0) + i32.const 0 ;; payload_len + call $send + ) + ) + "#; + 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 inbox = rt.new_inbox::().unwrap(); + let addr = rt.spawn(actor).unwrap(); + + // Build a framed message with the inbox address as the first 32 bytes + let msg = framed_msg(inbox.addr(), b"ignored-payload"); + rt.send_to(addr, msg).unwrap(); + rt.tick(); + + let received = inbox.try_recv().expect("should receive zero-length message"); + assert!(received.0.is_empty(), "payload should be empty"); +} + +// ── Multiple sequential traps: actor survives repeated failures ────────────── + +#[test] +fn actor_survives_multiple_sequential_traps() { + // After 3 consecutive traps, the actor should still be alive and + // able to process a non-trapping message. + let wat = r#" + (module + (import "swactor" "send" (func $send (param i32 i32 i32))) + (memory (export "memory") 1) + (global $counter (mut i32) (i32.const 0)) + (func (export "alloc") (param i32) (result i32) + i32.const 256 + ) + (func (export "handle") (param $ptr i32) (param $len i32) + global.get $counter + i32.const 3 + i32.lt_u + if + ;; First 3 calls: trap + global.get $counter + i32.const 1 + i32.add + global.set $counter + unreachable + end + ;; 4th+ call: echo the message back using first 32 bytes as dest + local.get $ptr + local.get $ptr + i32.const 32 + i32.add + local.get $len + i32.const 32 + i32.sub + call $send + ) + ) + "#; + 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 inbox = rt.new_inbox::().unwrap(); + let addr = rt.spawn(actor).unwrap(); + + // 3 trapping messages + for _ in 0..3 { + rt.send_to(addr, framed_msg(inbox.addr(), b"will trap")).unwrap(); + rt.tick(); + assert!(inbox.try_recv().is_none(), "trapped call should produce nothing"); + } + + // 4th message: should succeed + let payload = b"survived"; + rt.send_to(addr, framed_msg(inbox.addr(), payload)).unwrap(); + rt.tick(); + + let received = inbox.try_recv().expect("actor should work after multiple traps"); + assert_eq!(received.0, payload); +} + // ── Multi-worker: WASM actors across threads ───────────────────────────────── #[test]