diff --git a/crates/wasm-actor/tests/wasm_actor.rs b/crates/wasm-actor/tests/wasm_actor.rs index 36008d2..1137d92 100644 --- a/crates/wasm-actor/tests/wasm_actor.rs +++ b/crates/wasm-actor/tests/wasm_actor.rs @@ -4907,7 +4907,7 @@ proptest! { .build() .unwrap(); let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); + let _inbox = rt.new_inbox::().unwrap(); let addr = rt.spawn(actor).unwrap(); // Send raw payload (not framed) — echo will try to use first 32 bytes as dest @@ -5981,3 +5981,146 @@ fn send_to_dead_wasm_actor_silently_dropped() { rt.tick(); assert!(inbox.try_recv().is_none(), "dead actor should not deliver"); } + +// ── Guest alloc always returns same pointer — messages overwrite each other ── + +#[test] +fn static_alloc_pointer_messages_overwrite() { + // Guest alloc always returns 4096. Each message overwrites the same region. + // The last message to be processed wins. Verifies outbox snapshots correctly. + 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(); + + // Send 3 messages in same tick. Each writes to offset 4096. + // Echo reads from 4096 — should read the data written for that specific call + // because outbox snapshots at send time. + rt.send_to(addr, framed_msg(inbox.addr(), b"first")).unwrap(); + rt.send_to(addr, framed_msg(inbox.addr(), b"second")).unwrap(); + rt.send_to(addr, framed_msg(inbox.addr(), b"third")).unwrap(); + rt.tick(); + + let mut payloads = Vec::new(); + while let Some(msg) = inbox.try_recv() { + payloads.push(msg.0); + } + assert_eq!(payloads.len(), 3); + assert_eq!(payloads[0], b"first"); + assert_eq!(payloads[1], b"second"); + assert_eq!(payloads[2], b"third"); +} + +// ── Module with nested blocks ─────────────────────────────────────────────── + +#[test] +fn nested_blocks_in_handle() { + // Guest uses nested block/end to structure control flow. + 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 4096) + (func (export "handle") (param $ptr i32) (param $len i32) + (block $outer + (block $inner + ;; If len <= 32 (no payload), break to outer (skip send) + (br_if $outer (i32.le_s (local.get $len) (i32.const 32))) + ;; If len > 64 (big payload), break to inner (send "BIG") + (br_if $inner (i32.gt_s (local.get $len) (i32.const 64))) + ;; Small payload: send "SM" + (i32.store8 (i32.const 200) (i32.const 83)) + (i32.store8 (i32.const 201) (i32.const 77)) + (call $send (local.get $ptr) (i32.const 200) (i32.const 2)) + return + ) + ;; Big payload + (i32.store8 (i32.const 200) (i32.const 66)) + (i32.store8 (i32.const 201) (i32.const 71)) + (call $send (local.get $ptr) (i32.const 200) (i32.const 2)) + ) + ) + ) + "#; + 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(); + + // No payload (len=32) → no send + rt.send_to(addr, framed_msg(inbox.addr(), b"")).unwrap(); + // Small payload (5 bytes, total len=37) → "SM" + rt.send_to(addr, framed_msg(inbox.addr(), b"hello")).unwrap(); + // Big payload (50 bytes, total len=82) → "BG" + rt.send_to(addr, framed_msg(inbox.addr(), &[b'x'; 50])).unwrap(); + rt.tick(); + + let msg1 = inbox.try_recv().unwrap(); + let msg2 = inbox.try_recv().unwrap(); + assert!(inbox.try_recv().is_none(), "empty payload should not send"); + assert_eq!(msg1.0, b"SM"); + assert_eq!(msg2.0, b"BG"); +} + +// ── Guest uses memory.size to check available memory ──────────────────────── + +#[test] +fn guest_uses_memory_size_instruction() { + // Guest checks memory.size and sends it as a response byte. + let wat = r#" + (module + (import "swactor" "send" (func $send (param i32 i32 i32))) + (memory (export "memory") 2) + (func (export "alloc") (param i32) (result i32) i32.const 4096) + (func (export "handle") (param $ptr i32) (param $len i32) + ;; memory.size returns pages (should be 2) + (i32.store8 (i32.const 200) (memory.size)) + (call $send (local.get $ptr) (i32.const 200) (i32.const 1)) + ) + ) + "#; + 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"size")).unwrap(); + rt.tick(); + + let msg = inbox.try_recv().unwrap(); + assert_eq!(msg.0[0], 2, "memory.size should report 2 pages"); +} + +// ── Property: spawn-send-tick-stop cycle never panics ─────────────────────── + +proptest! { + #[test] + fn prop_spawn_send_stop_cycle_never_panics( + n_msgs in 0u8..20, + payload_len in 0usize..128, + ) { + 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(); + + let payload = vec![0xAB; payload_len]; + for _ in 0..n_msgs { + let _ = rt.send_to(addr, framed_msg(inbox.addr(), &payload)); + } + rt.tick(); + rt.stop_actor(addr); + rt.tick(); + // Drain inbox + while let Some(_) = inbox.try_recv() {} + } +}