From 4bc6f6077d64fbe41275b12a96210504bfd1c6c5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Feb 2026 08:50:42 +0000 Subject: [PATCH] =?UTF-8?q?test:=20Cycle=205=20=E2=80=94=20type=20mismatch?= =?UTF-8?q?,=20state=20persistence,=20dynamic=20spawn?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Non-ByteMessage to WASM actor is silently ignored (type mismatch) - Guest mutable global state persists across handle() calls (counter) - Native handler can spawn WASM actor dynamically via ctx.spawn() Authored by Claude, lovingly guided by Zachery Aaron Shores-Chmielewski --- crates/wasm-actor/tests/wasm_actor.rs | 138 ++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/crates/wasm-actor/tests/wasm_actor.rs b/crates/wasm-actor/tests/wasm_actor.rs index dc03d06..05e86a4 100644 --- a/crates/wasm-actor/tests/wasm_actor.rs +++ b/crates/wasm-actor/tests/wasm_actor.rs @@ -1064,6 +1064,144 @@ fn overlapping_dest_and_payload_in_send_works() { assert_eq!(received.0.len(), 32, "payload should be 32 bytes from overlapping region"); } +// ── Type mismatch: non-ByteMessage sent to WASM actor ──────────────────────── + +#[test] +fn non_byte_message_to_wasm_actor_is_silently_ignored() { + // Sending a message of the wrong type (not ByteMessage) to a WASM actor. + // The runtime's handle_any downcast fails, counting a type mismatch. + // The actor should survive and still process valid ByteMessages. + 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(); + + // Send wrong type — u32 instead of ByteMessage + // This goes through send_any with Box::new(42u32), downcast to ByteMessage fails. + rt.send_to(addr, 42u32).unwrap(); + rt.tick(); // type mismatch — silently ignored + + // Actor still alive — send a valid message + let payload = b"after mismatch"; + rt.send_to(addr, framed_msg(inbox.addr(), payload)).unwrap(); + rt.tick(); + + let received = inbox.try_recv().expect("actor should work after type mismatch"); + assert_eq!(received.0, payload); +} + +// ── Guest state persistence: mutable global survives across messages ───────── + +#[test] +fn guest_mutable_state_persists_across_messages() { + // A guest module with a mutable global counter. Each handle call increments + // the counter and includes it in the reply payload. Verifies that the + // wasmtime Store and linear memory persist between handle() calls. + let wat = r#" + (module + (import "swactor" "send" (func $send (param i32 i32 i32))) + (memory (export "memory") 1) + (global $counter (mut i32) (i32.const 0)) + + (func (export "alloc") (param i32) (result i32) + i32.const 256 + ) + (func (export "handle") (param $ptr i32) (param $len i32) + ;; Increment counter + global.get $counter + i32.const 1 + i32.add + global.set $counter + + ;; Write counter value to memory at offset 200 + (i32.store8 (i32.const 200) (global.get $counter)) + + ;; Send counter byte as payload to dest at $ptr + local.get $ptr ;; dest_ptr (first 32 bytes of message) + i32.const 200 ;; payload_ptr (counter byte) + i32.const 1 ;; payload_len + 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::().unwrap(); + let addr = rt.spawn(actor).unwrap(); + + // Send 3 messages, each should get an incrementing counter + for expected in 1..=3u8 { + rt.send_to(addr, framed_msg(inbox.addr(), b"x")).unwrap(); + rt.tick(); + let received = inbox.try_recv().expect("should receive counter reply"); + assert_eq!(received.0, vec![expected], "counter should increment per message"); + } +} + +// ── Spawn WASM from handler: native actor spawns WASM actor during handle ──── + +struct WasmSpawner { + engine: SharedEngine, + wasm_bytes: Vec, +} + +#[derive(Clone)] +struct SpawnAndForward { + inbox_addr: ActorAddress, + payload: Vec, +} + +impl ActorInterface for WasmSpawner { + type Incoming = SpawnAndForward; + type Response = (); + fn handle(&mut self, ctx: &Ctx, msg: SpawnAndForward) { + let actor = WasmActorBuilder::new(self.engine.clone(), self.wasm_bytes.clone()) + .build() + .unwrap(); + let wasm_addr = ctx.spawn(actor); + let _ = ctx.send(wasm_addr.unwrap(), framed_msg(&msg.inbox_addr, &msg.payload)); + } +} + +#[test] +fn native_handler_spawns_wasm_actor_and_forwards_message() { + let engine = SharedEngine::new().unwrap(); + let wasm_bytes = guest_wasm("echo"); + + let spawner = WasmSpawner { + engine: engine.clone(), + wasm_bytes: wasm_bytes.clone(), + }; + + let rt = Runtime::new(RuntimeConfig::default()); + let inbox = rt.new_inbox::().unwrap(); + let spawner_addr = rt.spawn(spawner).unwrap(); + + rt.send_to( + spawner_addr, + SpawnAndForward { + inbox_addr: *inbox.addr(), + payload: b"spawned-echo".to_vec(), + }, + ) + .unwrap(); + + // Tick 1: Spawner receives message, spawns WASM actor, sends to it + rt.tick(); + // Tick 2: WASM actor processes message and echoes to inbox + rt.tick(); + + let received = inbox.try_recv().expect("dynamically spawned WASM actor should echo"); + assert_eq!(received.0, b"spawned-echo"); +} + // ── Multi-worker: WASM actors across threads ───────────────────────────────── #[test]