diff --git a/crates/wasm-actor/tests/wasm_actor.rs b/crates/wasm-actor/tests/wasm_actor.rs index 13f610e..dc03d06 100644 --- a/crates/wasm-actor/tests/wasm_actor.rs +++ b/crates/wasm-actor/tests/wasm_actor.rs @@ -924,6 +924,146 @@ fn actor_survives_multiple_sequential_traps() { assert_eq!(received.0, payload); } +// ── Builder: module with start function that traps ─────────────────────────── + +#[test] +fn module_with_trapping_start_function_returns_error() { + // WASM modules can have a (start) function that runs during instantiation. + // If it traps, build() should return an error. + let wat = r#" + (module + (memory (export "memory") 1) + (func (export "alloc") (param i32) (result i32) i32.const 256) + (func (export "handle") (param i32 i32)) + (func $init unreachable) + (start $init) + ) + "#; + let wasm = wat::parse_str(wat).unwrap(); + let engine = SharedEngine::new().unwrap(); + let result = WasmActorBuilder::new(engine, wasm).build(); + assert!(result.is_err(), "module with trapping start function should fail to build"); +} + +// ── Self-send: guest sends message back to own address ─────────────────────── + +#[test] +fn guest_self_send_creates_feedback_loop() { + // Echo guest sends its payload to a destination. If we set the dest + // to the actor's OWN address, it creates a feedback loop. The actor + // should process the self-sent message on the next tick. + 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(); + + // Frame: dest=self, payload=[inbox_addr | "hello"] + // Echo will send [inbox_addr | "hello"] back to itself. + // On next tick, it receives [inbox_addr | "hello"], echoes "hello" to inbox. + let inner_msg = framed_msg(inbox.addr(), b"hello"); + let self_msg = framed_msg(&addr, &inner_msg.0); + + rt.send_to(addr, self_msg).unwrap(); + rt.tick(); // actor echoes inner_msg to self + rt.tick(); // actor receives inner_msg, echoes "hello" to inbox + + let received = inbox.try_recv().expect("should receive after self-send loop"); + assert_eq!(received.0, b"hello"); +} + +// ── Amplification: guest sends many messages in one handle ─────────────────── + +#[test] +fn guest_sending_many_messages_in_one_handle_all_delivered() { + // A guest that calls swactor.send N times in a single handle call. + // All N messages should be delivered via the outbox drain. + 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 + ) + (func (export "handle") (param $ptr i32) (param $len i32) + ;; Send 10 messages, each with 0-byte payload + ;; dest_ptr = $ptr (first 32 bytes of the incoming message) + (local $i i32) + (local.set $i (i32.const 0)) + (block $break + (loop $loop + (br_if $break (i32.ge_u (local.get $i) (i32.const 10))) + (call $send (local.get $ptr) (i32.const 32) (i32.const 0)) + (local.set $i (i32.add (local.get $i) (i32.const 1))) + (br $loop) + ) + ) + ) + ) + "#; + 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(); + + rt.send_to(addr, framed_msg(inbox.addr(), b"x")).unwrap(); + rt.tick(); + + let mut count = 0; + while inbox.try_recv().is_some() { + count += 1; + } + assert_eq!(count, 10, "guest should have sent exactly 10 messages"); +} + +// ── Overlapping send regions: dest_ptr and payload_ptr overlap ─────────────── + +#[test] +fn overlapping_dest_and_payload_in_send_works() { + // Guest calls send with dest_ptr=0, payload_ptr=16, payload_len=32. + // The dest region [0..32] and payload region [16..48] overlap. + // Both are read-only in the host, so this should work without corruption. + 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 + ) + (func (export "handle") (param $ptr i32) (param $len i32) + ;; Overlapping regions + local.get $ptr ;; dest_ptr (first 32 bytes of message) + local.get $ptr + i32.const 16 + i32.add ;; payload_ptr = ptr + 16 (overlaps with dest) + i32.const 32 ;; payload_len = 32 + 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 message where bytes [0..32] = inbox addr, [32..] = payload + // Guest reads dest from [0..32] (inbox addr) and payload from [16..48] + rt.send_to(addr, framed_msg(inbox.addr(), b"overlap-test-padding!")).unwrap(); + rt.tick(); + + let received = inbox.try_recv().expect("overlapping send should deliver"); + assert_eq!(received.0.len(), 32, "payload should be 32 bytes from overlapping region"); +} + // ── Multi-worker: WASM actors across threads ───────────────────────────────── #[test]