test: Cycle 10 — multi-dest sends, outbox copy safety, large payload, alloc-with-grow

Authored by Claude, lovingly guided by Zachery Aaron Shores-Chmielewski
This commit is contained in:
Claude 2026-02-13 09:00:58 +00:00
parent 1387bce562
commit c4a40953ca

View file

@ -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}"); 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::<ByteMessage>().unwrap();
let inbox_b = rt.new_inbox::<ByteMessage>().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::<ByteMessage>().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::<ByteMessage>().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<u8> = (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::<ByteMessage>().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);
}