diff --git a/crates/wasm-actor/tests/wasm_actor.rs b/crates/wasm-actor/tests/wasm_actor.rs index 9f763ae..208fe40 100644 --- a/crates/wasm-actor/tests/wasm_actor.rs +++ b/crates/wasm-actor/tests/wasm_actor.rs @@ -6929,4 +6929,220 @@ fn idle_ticks_dont_affect_wasm_actor() { let msg = inbox.try_recv().unwrap(); assert_eq!(msg.0, b"after-idle"); +} + +// ── WASM + native actors alternate processing in same tick ────────────────── + +#[test] +fn wasm_and_native_alternate_in_same_tick() { + struct NativeEcho2; + impl ActorInterface for NativeEcho2 { + type Incoming = ByteMessage; + type Response = (); + fn handle(&mut self, ctx: &Ctx, msg: ByteMessage) { + if msg.0.len() >= 32 { + let mut addr_bytes = [0u8; 32]; + addr_bytes.copy_from_slice(&msg.0[..32]); + let dest = ActorAddress(addr_bytes); + let payload = msg.0[32..].to_vec(); + let _ = ctx.send(dest, ByteMessage(payload)); + } + } + } + + let engine = SharedEngine::new().unwrap(); + let wasm_echo = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); + + let rt = Runtime::new(RuntimeConfig::default()); + let inbox = rt.new_inbox::().unwrap(); + let wasm_addr = rt.spawn(wasm_echo).unwrap(); + let native_addr = rt.spawn(NativeEcho2).unwrap(); + + for i in 0u8..10 { + if i % 2 == 0 { + rt.send_to(wasm_addr, framed_msg(inbox.addr(), &[i])).unwrap(); + } else { + rt.send_to(native_addr, framed_msg(inbox.addr(), &[i])).unwrap(); + } + } + rt.tick(); + + let mut received = Vec::new(); + while let Some(msg) = inbox.try_recv() { + received.push(msg.0[0]); + } + received.sort(); + let expected: Vec = (0..10).collect(); + assert_eq!(received, expected, "all 10 messages from both types should deliver"); +} + +// ── Guest sums two payload bytes ──────────────────────────────────────────── + +#[test] +fn guest_sums_two_payload_bytes() { + 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) + (local $val i32) + (local.set $val + (i32.add + (i32.load8_u (i32.add (local.get $ptr) (i32.const 32))) + (i32.load8_u (i32.add (local.get $ptr) (i32.const 33))) + ) + ) + (i32.store8 (i32.const 200) (local.get $val)) + (i32.store8 (i32.const 201) (local.get $val)) + (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(); + + rt.send_to(addr, framed_msg(inbox.addr(), &[30, 12])).unwrap(); + rt.tick(); + + let msg = inbox.try_recv().unwrap(); + assert_eq!(msg.0, vec![42, 42], "30+12=42 duplicated"); +} + +// ── Single byte payload echo ──────────────────────────────────────────────── + +#[test] +fn single_byte_payload_echo() { + 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(); + + rt.send_to(addr, framed_msg(inbox.addr(), &[0x42])).unwrap(); + rt.tick(); + + let msg = inbox.try_recv().unwrap(); + assert_eq!(msg.0, vec![0x42]); +} + +// ── Module with no functions fails ────────────────────────────────────────── + +#[test] +fn module_with_no_functions_fails() { + let wat = "(module (memory (export \"memory\") 1))"; + let wasm = wat::parse_str(wat).unwrap(); + let engine = SharedEngine::new().unwrap(); + assert!(WasmActorBuilder::new(engine, wasm).build().is_err()); +} + +// ── Response varies by message size ───────────────────────────────────────── + +#[test] +fn response_varies_by_message_size() { + 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) + (i32.store8 (i32.const 200) (i32.sub (local.get $len) (i32.const 32))) + (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(), &[])).unwrap(); + rt.send_to(addr, framed_msg(inbox.addr(), &[1])).unwrap(); + rt.send_to(addr, framed_msg(inbox.addr(), &[0; 50])).unwrap(); + rt.send_to(addr, framed_msg(inbox.addr(), &[0; 200])).unwrap(); + rt.tick(); + + let sizes: Vec = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0[0])).collect(); + assert_eq!(sizes, vec![0, 1, 50, 200]); +} + +// ── Property: echo is deterministic ───────────────────────────────────────── + +proptest! { + #[test] + fn prop_echo_is_deterministic( + payload in proptest::collection::vec(proptest::num::u8::ANY, 0..128) + ) { + let engine = SharedEngine::new().unwrap(); + let rt = Runtime::new(RuntimeConfig::default()); + let inbox = rt.new_inbox::().unwrap(); + + let actor1 = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); + let addr1 = rt.spawn(actor1).unwrap(); + rt.send_to(addr1, framed_msg(inbox.addr(), &payload)).unwrap(); + rt.tick(); + let result1 = inbox.try_recv().map(|m| m.0); + + let actor2 = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); + let addr2 = rt.spawn(actor2).unwrap(); + rt.send_to(addr2, framed_msg(inbox.addr(), &payload)).unwrap(); + rt.tick(); + let result2 = inbox.try_recv().map(|m| m.0); + + assert_eq!(result1, result2, "same input should produce same output"); + } +} + +// ── 200th test: comprehensive lifecycle with all guest types ──────────────── + +#[test] +fn comprehensive_lifecycle_all_guest_types_200th() { + // Spawn one of each guest type (echo, double, silent), send messages, + // verify outputs, stop them all, check cleanup. + let engine = SharedEngine::new().unwrap(); + let echo = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); + let double = WasmActorBuilder::new(engine.clone(), guest_wasm("double")).build().unwrap(); + let silent = WasmActorBuilder::new(engine, guest_wasm("silent")).build().unwrap(); + + let rt = Runtime::new(RuntimeConfig::default()); + let inbox = rt.new_inbox::().unwrap(); + + let echo_addr = rt.spawn(echo).unwrap(); + let double_addr = rt.spawn(double).unwrap(); + let silent_addr = rt.spawn(silent).unwrap(); + + // Send to all three + rt.send_to(echo_addr, framed_msg(inbox.addr(), b"E")).unwrap(); + rt.send_to(double_addr, framed_msg(inbox.addr(), b"D")).unwrap(); + rt.send_to(silent_addr, framed_msg(inbox.addr(), b"S")).unwrap(); + rt.tick(); + + let mut payloads: Vec> = Vec::new(); + while let Some(msg) = inbox.try_recv() { + payloads.push(msg.0); + } + payloads.sort(); + // Echo → "E" (1x), Double → "D" (2x), Silent → nothing + assert_eq!(payloads, vec![b"D".to_vec(), b"D".to_vec(), b"E".to_vec()]); + + // Stop all + rt.stop_actor(echo_addr); + rt.stop_actor(double_addr); + rt.stop_actor(silent_addr); + rt.tick(); + rt.tick(); + + // Send to stopped actors — silently dropped + rt.send_to(echo_addr, framed_msg(inbox.addr(), b"gone")).ok(); + rt.tick(); + assert!(inbox.try_recv().is_none(), "stopped actors should not deliver"); } \ No newline at end of file