diff --git a/crates/wasm-actor/tests/wasm_actor.rs b/crates/wasm-actor/tests/wasm_actor.rs index afd6868..f51d2eb 100644 --- a/crates/wasm-actor/tests/wasm_actor.rs +++ b/crates/wasm-actor/tests/wasm_actor.rs @@ -9755,4 +9755,186 @@ fn engine_dropped_after_build_actor_still_works() { let resp = inbox.try_recv().expect("should echo after engine dropped"); assert_eq!(resp.0, b"after-drop"); +} + +// ── Cycle 67 ───────────────────────────────────────────────────────────────── + +// Guest uses grow, then accesses the newly grown page +#[test] +fn guest_grows_memory_and_uses_new_page() { + let wat = r#"(module + (import "swactor" "send" (func $send (param i32 i32 i32))) + (memory (export "memory") 1) + (func (export "alloc") (param $len i32) (result i32) i32.const 1024) + (func (export "handle") (param $ptr i32) (param $len i32) + (local $old_pages i32) + ;; Grow by 1 page + (local.set $old_pages (memory.grow (i32.const 1))) + ;; Write at start of new page (old_pages * 65536) + (i32.store + (i32.mul (local.get $old_pages) (i32.const 65536)) + (i32.const 0xDEADBEEF) + ) + ;; Send from first 32 bytes of message + (if (i32.ge_u (local.get $len) (i32.const 32)) + (then + (call $send (local.get $ptr) (i32.const 0) (i32.const 0)) + ) + ) + ) + )"#; + let engine = SharedEngine::new().unwrap(); + let actor = WasmActorBuilder::new(engine, wat::parse_str(wat).unwrap()) + .build().unwrap(); + let rt = Runtime::new(RuntimeConfig::default()); + let inbox = rt.new_inbox::().unwrap(); + let addr = rt.spawn(actor).unwrap(); + + let mut msg = Vec::new(); + msg.extend_from_slice(&inbox.addr().0); + rt.send_to(addr, ByteMessage(msg)).unwrap(); + rt.tick(); + + // Should receive empty payload (sent 0 bytes) + let resp = inbox.try_recv().expect("should receive msg after grow"); + assert!(resp.0.is_empty()); +} + +// Native actor receives message from WASM echo, verifies payload +struct PayloadVerifier { + expected: Vec, + verified: std::cell::Cell, +} +impl ActorInterface for PayloadVerifier { + type Incoming = ByteMessage; + type Response = (); + fn handle(&mut self, _ctx: &Ctx, msg: ByteMessage) { + if msg.0 == self.expected { + self.verified.set(true); + } + } +} + +#[test] +fn native_verifier_receives_wasm_echo() { + let engine = SharedEngine::new().unwrap(); + let echo = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); + let rt = Runtime::new(RuntimeConfig::default()); + + let verifier = PayloadVerifier { + expected: b"hello-from-wasm".to_vec(), + verified: std::cell::Cell::new(false), + }; + let v_addr = rt.spawn(verifier).unwrap(); + let e_addr = rt.spawn(echo).unwrap(); + + rt.send_to(e_addr, framed_msg(&v_addr, b"hello-from-wasm")).unwrap(); + rt.tick(); + rt.tick(); // verifier processes the echoed message +} + +// Sequential spawn-echo-stop for 3 different guest types +#[test] +fn sequential_echo_double_silent_lifecycle() { + let engine = SharedEngine::new().unwrap(); + let rt = Runtime::new(RuntimeConfig::default()); + let inbox = rt.new_inbox::().unwrap(); + + // Echo phase + let echo = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); + let e_addr = rt.spawn(echo).unwrap(); + rt.send_to(e_addr, framed_msg(inbox.addr(), b"E")).unwrap(); + rt.tick(); + assert_eq!(inbox.try_recv().unwrap().0, b"E"); + rt.stop_actor(e_addr); + rt.tick(); + + // Double phase + let double = WasmActorBuilder::new(engine.clone(), guest_wasm("double")).build().unwrap(); + let d_addr = rt.spawn(double).unwrap(); + rt.send_to(d_addr, framed_msg(inbox.addr(), b"D")).unwrap(); + rt.tick(); + let d_msgs: Vec<_> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); + assert_eq!(d_msgs.len(), 2); + rt.stop_actor(d_addr); + rt.tick(); + + // Silent phase + let silent = WasmActorBuilder::new(engine, guest_wasm("silent")).build().unwrap(); + let s_addr = rt.spawn(silent).unwrap(); + rt.send_to(s_addr, ByteMessage(b"S".to_vec())).unwrap(); + rt.tick(); + assert!(inbox.try_recv().is_none()); + rt.stop_actor(s_addr); + rt.tick(); +} + +// Guest with 0 initial data pages but minimum 1 required → module with 0 pages +#[test] +fn module_with_zero_memory_pages_builds_but_alloc_fails_gracefully() { + // Can't have 0-page memory in WAT (minimum is 0 but the export needs at least some) + // Actually (memory 0) is valid — 0 initial pages, can grow later + let wat = r#"(module + (memory (export "memory") 0) + (func (export "alloc") (param $len i32) (result i32) i32.const -1) + (func (export "handle") (param $ptr i32) (param $len i32)) + )"#; + let engine = SharedEngine::new().unwrap(); + let actor = WasmActorBuilder::new(engine, wat::parse_str(wat).unwrap()) + .build().unwrap(); + let rt = Runtime::new(RuntimeConfig::default()); + let addr = rt.spawn(actor).unwrap(); + + // alloc returns -1, so message is dropped (ptr < 0 check) + rt.send_to(addr, ByteMessage(b"test".to_vec())).unwrap(); + rt.tick(); // no panic +} + +// Double guest receives same message twice in same tick — produces 4 responses +#[test] +fn double_receives_two_messages_produces_four() { + let engine = SharedEngine::new().unwrap(); + let double = WasmActorBuilder::new(engine, guest_wasm("double")).build().unwrap(); + let rt = Runtime::new(RuntimeConfig::default()); + let inbox = rt.new_inbox::().unwrap(); + let addr = rt.spawn(double).unwrap(); + + rt.send_to(addr, framed_msg(inbox.addr(), b"A")).unwrap(); + rt.send_to(addr, framed_msg(inbox.addr(), b"B")).unwrap(); + rt.tick(); + + let msgs: Vec> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); + assert_eq!(msgs.len(), 4, "2 messages × 2 copies each = 4"); + // First two are copies of "A", last two are copies of "B" + assert_eq!(msgs[0], b"A"); + assert_eq!(msgs[1], b"A"); + assert_eq!(msgs[2], b"B"); + assert_eq!(msgs[3], b"B"); +} + +// Property: stopping an actor never panics regardless of pending messages +proptest! { + #[test] + fn prop_stop_with_pending_never_panics( + n_msgs in 0usize..50, + ticks_before_stop in 0usize..5, + ticks_after_stop in 1usize..5, + ) { + 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(); + + for i in 0..n_msgs { + let _ = rt.send_to(addr, ByteMessage(vec![i as u8; 10])); + } + for _ in 0..ticks_before_stop { + rt.tick(); + } + rt.stop_actor(addr); + for _ in 0..ticks_after_stop { + rt.tick(); + } + } } \ No newline at end of file