test: Cycle 5 — type mismatch, state persistence, dynamic spawn

- 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
This commit is contained in:
Claude 2026-02-13 08:50:42 +00:00
parent 06aadeb911
commit 4bc6f6077d

View file

@ -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"); 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::<ByteMessage>().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::<ByteMessage>().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<u8>,
}
#[derive(Clone)]
struct SpawnAndForward {
inbox_addr: ActorAddress,
payload: Vec<u8>,
}
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::<ByteMessage>().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 ───────────────────────────────── // ── Multi-worker: WASM actors across threads ─────────────────────────────────
#[test] #[test]