From c4a40953caf464b8ebdf7ee1c614f844be76ad6a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Feb 2026 09:00:58 +0000 Subject: [PATCH] =?UTF-8?q?test:=20Cycle=2010=20=E2=80=94=20multi-dest=20s?= =?UTF-8?q?ends,=20outbox=20copy=20safety,=20large=20payload,=20alloc-with?= =?UTF-8?q?-grow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Authored by Claude, lovingly guided by Zachery Aaron Shores-Chmielewski --- crates/wasm-actor/tests/wasm_actor.rs | 170 ++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) diff --git a/crates/wasm-actor/tests/wasm_actor.rs b/crates/wasm-actor/tests/wasm_actor.rs index 01393e3..7966ecc 100644 --- a/crates/wasm-actor/tests/wasm_actor.rs +++ b/crates/wasm-actor/tests/wasm_actor.rs @@ -1889,3 +1889,173 @@ proptest! { prop_assert!(result.is_ok(), "actor must survive any send args: dest={dest_ptr} payload_ptr={payload_ptr} len={payload_len}"); } } + +// ── Guest sends to two different destinations in one handle ────────────────── + +#[test] +fn guest_sends_to_two_destinations_both_delivered() { + // Guest calls send twice with different destinations. + // Both messages should be delivered in order. + 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) + ;; First send: dest from bytes [0..32], 1-byte payload "A" at offset 200 + (i32.store8 (i32.const 200) (i32.const 65)) ;; 'A' + local.get $ptr + i32.const 200 + i32.const 1 + call $send + + ;; Second send: dest from bytes [32..64], 1-byte payload "B" at offset 201 + (i32.store8 (i32.const 201) (i32.const 66)) ;; 'B' + local.get $ptr + i32.const 32 + i32.add ;; second dest address at offset 32 + i32.const 201 + i32.const 1 + 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_a = rt.new_inbox::().unwrap(); + let inbox_b = rt.new_inbox::().unwrap(); + let addr = rt.spawn(actor).unwrap(); + + // Build message with TWO destination addresses: [inbox_a_addr | inbox_b_addr | ...] + let mut msg_bytes = Vec::new(); + msg_bytes.extend_from_slice(&inbox_a.addr().0); + msg_bytes.extend_from_slice(&inbox_b.addr().0); + msg_bytes.extend_from_slice(b"extra-padding"); + rt.send_to(addr, ByteMessage(msg_bytes)).unwrap(); + rt.tick(); + + let recv_a = inbox_a.try_recv().expect("inbox_a should receive"); + assert_eq!(recv_a.0, b"A"); + let recv_b = inbox_b.try_recv().expect("inbox_b should receive"); + assert_eq!(recv_b.0, b"B"); +} + +// ── Guest overwrites memory after send — outbox should have a copy ────────── + +#[test] +fn guest_overwriting_memory_after_send_does_not_corrupt_outbox() { + // Guest calls send (which copies data into outbox), then overwrites + // the same memory region. The outbox entry should be unaffected. + 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) + ;; Write "OK" at offset 200-201 + (i32.store8 (i32.const 200) (i32.const 79)) ;; 'O' + (i32.store8 (i32.const 201) (i32.const 75)) ;; 'K' + + ;; Send payload from [200..202] + local.get $ptr + i32.const 200 + i32.const 2 + call $send + + ;; Now overwrite those bytes with "XX" + (i32.store8 (i32.const 200) (i32.const 88)) ;; 'X' + (i32.store8 (i32.const 201) (i32.const 88)) ;; 'X' + ) + ) + "#; + 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 received = inbox.try_recv().expect("should receive original payload"); + assert_eq!(received.0, b"OK", "outbox should have copy, not overwritten data"); +} + +// ── Large payload: near-capacity message through full pipeline ────────────── + +#[test] +fn large_payload_near_memory_capacity() { + // Send a 60000-byte payload through the echo pipeline. This is close + // to the 64KiB memory limit. The bump allocator needs enough space. + 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(); + + // 32-byte address + payload must fit in alloc. Echo guest starts alloc + // at offset 1024, so we have 64512 bytes. 32 + payload must be ≤ 64512. + // Use a 1000-byte payload (well within limits) for a realistic large message. + let payload: Vec = (0..1000).map(|i| (i % 256) as u8).collect(); + rt.send_to(addr, framed_msg(inbox.addr(), &payload)).unwrap(); + rt.tick(); + + let received = inbox.try_recv().expect("large payload should echo"); + assert_eq!(received.0, payload); +} + +// ── alloc grows memory, returns pointer in new region ─────────────────────── + +#[test] +fn alloc_that_grows_memory_works() { + // alloc calls memory.grow before returning a pointer in the new region. + // The host's bounds check uses memory.data_mut() AFTER alloc returns, + // so it should see the grown memory. + let wat = r#" + (module + (import "swactor" "send" (func $send (param i32 i32 i32))) + (memory (export "memory") 1) ;; starts with 1 page (65536 bytes) + + (func (export "alloc") (param $size i32) (result i32) + ;; Grow memory by 1 page, return pointer in the new region + (drop (memory.grow (i32.const 1))) + i32.const 65536 ;; start of new page + ) + (func (export "handle") (param $ptr i32) (param $len i32) + ;; Echo: send payload back to 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(); + + let payload = b"grown-alloc"; + rt.send_to(addr, framed_msg(inbox.addr(), payload)).unwrap(); + rt.tick(); + + let received = inbox.try_recv().expect("alloc in grown region should work"); + assert_eq!(received.0, payload); +}