From bd089de6667f848a823b7852a55a694af5fd0392 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Feb 2026 09:45:28 +0000 Subject: [PATCH] =?UTF-8?q?test:=20Cycle=2038=20=E2=80=94=20native=20spawn?= =?UTF-8?q?s=20WASM=20inline,=20many=20locals,=201000=20ticks,=20send-to-d?= =?UTF-8?q?ead?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - native_handler_spawns_wasm_actor_inline: native handler spawns WASM actor + sends - handle_uses_many_locals: guest uses 4 local variables for arithmetic - wasm_actor_survives_1000_ticks: 1000+ empty ticks + 10 messages, no resource leak - send_to_dead_wasm_actor_silently_dropped: send to stopped actor doesn't panic All 162 tests pass. No new bugs found. 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 1df7f8c..36008d2 100644 --- a/crates/wasm-actor/tests/wasm_actor.rs +++ b/crates/wasm-actor/tests/wasm_actor.rs @@ -5843,3 +5843,141 @@ proptest! { // No panic = success. Message either delivers or is silently dropped. } } + +// ── Spawn WASM actor from inside native actor's handle ────────────────────── + +#[test] +fn native_handler_spawns_wasm_actor_inline() { + // A native actor receives a message and spawns a WASM echo actor in its handler, + // then sends a message to the newly spawned actor. + struct InlineSpawner { + engine: SharedEngine, + wasm_bytes: Vec, + result_inbox: ActorAddress, + } + impl ActorInterface for InlineSpawner { + type Incoming = ByteMessage; + type Response = (); + fn handle(&mut self, ctx: &Ctx, _msg: ByteMessage) { + let actor = WasmActorBuilder::new(self.engine.clone(), self.wasm_bytes.clone()) + .build() + .unwrap(); + let wasm_addr = ctx.spawn(actor).unwrap(); + let _ = ctx.send(wasm_addr, framed_msg(&self.result_inbox, b"from-spawner")); + } + } + + let engine = SharedEngine::new().unwrap(); + let rt = Runtime::new(RuntimeConfig::default()); + let inbox = rt.new_inbox::().unwrap(); + + let spawner = InlineSpawner { + engine, + wasm_bytes: guest_wasm("echo"), + result_inbox: *inbox.addr(), + }; + let spawner_addr = rt.spawn(spawner).unwrap(); + + rt.send_to(spawner_addr, ByteMessage(vec![])).unwrap(); + rt.tick(); // spawner creates WASM actor + sends message + rt.tick(); // WASM actor processes message, echoes to inbox + + let msg = inbox.try_recv().expect("spawned WASM actor should echo to inbox"); + assert_eq!(msg.0, b"from-spawner"); +} + +// ── WASM actor with local variables (stack manipulation) ──────────────────── + +#[test] +fn handle_uses_many_locals() { + // Guest uses several local variables for computation. + 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 $a i32) (local $b i32) (local $c i32) (local $d i32) + (local.set $a (i32.const 10)) + (local.set $b (i32.const 20)) + (local.set $c (i32.add (local.get $a) (local.get $b))) + (local.set $d (i32.mul (local.get $c) (i32.const 2))) + ;; d = (10 + 20) * 2 = 60 + (i32.store8 (i32.const 200) (local.get $d)) + (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(), b"compute")).unwrap(); + rt.tick(); + + let msg = inbox.try_recv().unwrap(); + assert_eq!(msg.0[0], 60, "(10+20)*2 = 60"); +} + +// ── WASM actor after many ticks still functions (no resource leak) ────────── + +#[test] +fn wasm_actor_survives_1000_ticks() { + 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 a message every 100 ticks + for i in 0u8..10 { + // 100 empty ticks + for _ in 0..100 { + rt.tick(); + } + rt.send_to(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]); + } + let expected: Vec = (0..10).collect(); + assert_eq!(received, expected, "actor should still work after 1000+ ticks"); +} + +// ── Send to dead actor — message silently dropped ─────────────────────────── + +#[test] +fn send_to_dead_wasm_actor_silently_dropped() { + 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(); + + // Verify it works + rt.send_to(addr, framed_msg(inbox.addr(), b"alive")).unwrap(); + rt.tick(); + assert!(inbox.try_recv().is_some()); + + // Stop and cleanup + rt.stop_actor(addr); + rt.tick(); + rt.tick(); + + // Send to dead actor — should not panic + let result = rt.send_to(addr, framed_msg(inbox.addr(), b"dead")); + // Either returns Ok (message silently dropped) or Err (dead address) + // Both are acceptable — the key is no panic + drop(result); + rt.tick(); + assert!(inbox.try_recv().is_none(), "dead actor should not deliver"); +}