test: Cycle 4 — start trap, self-send, amplification, overlapping send

- Module with trapping start function correctly rejected by builder
- Self-send feedback loop works (echo to own address, relay on next tick)
- Guest sending 10 messages in one handle: all 10 delivered via outbox
- Overlapping dest_ptr and payload_ptr in send: reads are independent

Authored by Claude, lovingly guided by Zachery Aaron Shores-Chmielewski
This commit is contained in:
Claude 2026-02-13 08:48:39 +00:00
parent 1be0281056
commit 06aadeb911

View file

@ -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::<ByteMessage>().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::<ByteMessage>().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::<ByteMessage>().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]