From 8e8ea693a63169eb81f569e23cdabeef5296ac9b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Feb 2026 09:49:38 +0000 Subject: [PATCH] =?UTF-8?q?test:=20Cycle=2041=20=E2=80=94=20address-only?= =?UTF-8?q?=20msg,=20last=20byte,=20overlapping=20alloc,=20wrong=20name,?= =?UTF-8?q?=20fan-in?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - handle_receives_just_address_no_payload: 32-byte message echos empty payload - guest_writes_last_byte_of_memory: write/read at offset 65535 - alloc_returns_overlapping_region: static alloc region, outbox snapshots correctly - memory_exported_with_wrong_name_fails: memory as "heap" not "memory" rejected - fan_in_from_multiple_native_senders: 5 inboxes all receive correct echo responses All 175 tests pass. No new bugs found. Authored by Claude, lovingly guided by Zachery Aaron Shores-Chmielewski --- crates/wasm-actor/tests/wasm_actor.rs | 140 ++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) diff --git a/crates/wasm-actor/tests/wasm_actor.rs b/crates/wasm-actor/tests/wasm_actor.rs index d63ad92..b5d9cc1 100644 --- a/crates/wasm-actor/tests/wasm_actor.rs +++ b/crates/wasm-actor/tests/wasm_actor.rs @@ -6260,3 +6260,143 @@ fn two_hundred_actors_from_same_engine() { } assert_eq!(count, 200, "all 200 actors should echo"); } + +// ── Handle receives exactly 32 bytes (just address, no payload) ───────────── + +#[test] +fn handle_receives_just_address_no_payload() { + // Send a message that is exactly 32 bytes (just the address header). + // The echo guest will try to send payload of len-32=0 bytes. + 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(); + + // framed_msg with empty payload = just 32-byte address + rt.send_to(addr, framed_msg(inbox.addr(), b"")).unwrap(); + rt.tick(); + + let msg = inbox.try_recv().expect("should receive empty echo"); + assert!(msg.0.is_empty(), "echo of empty payload should be empty"); +} + +// ── Guest writes to last byte of linear memory ────────────────────────────── + +#[test] +fn guest_writes_last_byte_of_memory() { + // Guest writes to offset 65535 (last byte of 1 page). + 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) + ;; Write to last byte of memory + (i32.store8 (i32.const 65535) (i32.const 77)) + ;; Send that byte + (call $send (local.get $ptr) (i32.const 65535) (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"x")).unwrap(); + rt.tick(); + + let msg = inbox.try_recv().unwrap(); + assert_eq!(msg.0, vec![77], "should read from last byte of memory"); +} + +// ── Alloc returns ptr in middle of previously allocated region ─────────────── + +#[test] +fn alloc_returns_overlapping_region() { + // Guest's alloc always returns 4096 regardless of previous calls. + // When the host writes message bytes, they always go to the same spot. + // Second message in same tick overwrites first message's data. + // But outbox snapshots, so both sends deliver their respective data. + 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) + ;; Echo: send payload back + (call $send + (local.get $ptr) + (i32.add (local.get $ptr) (i32.const 32)) + (i32.sub (local.get $len) (i32.const 32)) + ) + ) + ) + "#; + 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"AAA")).unwrap(); + rt.send_to(addr, framed_msg(inbox.addr(), b"BB")).unwrap(); + rt.tick(); + + let msg1 = inbox.try_recv().unwrap(); + let msg2 = inbox.try_recv().unwrap(); + // Both should reflect their original data despite using same alloc region + assert_eq!(msg1.0, b"AAA", "first echo should have first payload"); + assert_eq!(msg2.0, b"BB", "second echo should have second payload"); +} + +// ── Module exports memory with non-default name (should fail) ─────────────── + +#[test] +fn memory_exported_with_wrong_name_fails() { + let wat = r#" + (module + (import "swactor" "send" (func $send (param i32 i32 i32))) + (memory (export "heap") 1) + (func (export "alloc") (param i32) (result i32) i32.const 0) + (func (export "handle") (param i32 i32)) + ) + "#; + let wasm = wat::parse_str(wat).unwrap(); + let engine = SharedEngine::new().unwrap(); + let result = WasmActorBuilder::new(engine, wasm).build(); + assert!(result.is_err(), "memory exported as 'heap' should fail"); +} + +// ── WASM actor receives messages from multiple senders (fan-in) ───────────── + +#[test] +fn fan_in_from_multiple_native_senders() { + // 5 native actors all send to the same WASM echo actor. + // Echo sends responses back to their respective inboxes. + let engine = SharedEngine::new().unwrap(); + let echo = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); + + let rt = Runtime::new(RuntimeConfig::default()); + let echo_addr = rt.spawn(echo).unwrap(); + + let mut inboxes = Vec::new(); + for i in 0u8..5 { + let inbox = rt.new_inbox::().unwrap(); + rt.send_to(echo_addr, framed_msg(inbox.addr(), &[i])).unwrap(); + inboxes.push(inbox); + } + rt.tick(); + + for (i, inbox) in inboxes.iter().enumerate() { + let msg = inbox.try_recv().unwrap_or_else(|| panic!("inbox {i} should receive")); + assert_eq!(msg.0, vec![i as u8], "inbox {i} should get correct payload"); + } +}