From c304ad59603b86561d2771944520dce4ec21cf54 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Feb 2026 10:16:35 +0000 Subject: [PATCH] =?UTF-8?q?test(wasm-actor):=20cycle=2062=20=E2=80=94=20se?= =?UTF-8?q?lf-send=20budget,=20empty=20module,=20static=20alloc,=2032KB=20?= =?UTF-8?q?echo=20(270=20tests)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added 5 tests: self-send loop bounded by budget, empty module missing exports, static alloc same pointer echoes, two engines two runtimes cross-combo, 32KB payload round-trip. 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 2d9ade1..fc70ac6 100644 --- a/crates/wasm-actor/tests/wasm_actor.rs +++ b/crates/wasm-actor/tests/wasm_actor.rs @@ -9085,4 +9085,142 @@ proptest! { let msgs: Vec<_> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); assert_eq!(msgs.len(), n, "each actor echoes one message"); } +} + +// ── Cycle 62 ───────────────────────────────────────────────────────────────── + +// Guest that sends to itself (creates a feedback loop bounded by budget) +#[test] +fn self_send_loop_bounded_by_budget_detailed() { + // Similar to existing self_send test but with budget=2 and counting carefully + let engine = SharedEngine::new().unwrap(); + let actor = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); + let mut cfg = RuntimeConfig::default(); + cfg.actor_message_budget = 2; + let rt = Runtime::new(cfg); + let addr = rt.spawn(actor).unwrap(); + + // Echo actor echoes to the dest address embedded in the payload's first 32 bytes + // If we send framed_msg(addr, b"X"), the echo goes back to addr — self-loop! + rt.send_to(addr, framed_msg(&addr, b"X")).unwrap(); + + // Each tick processes up to budget=2 messages + for _ in 0..5 { + rt.tick(); + } + // Actor should still be alive (no crash) — the loop is infinite but budget-bounded +} + +// Guest module that is an empty module (no memory, no functions) +#[test] +fn empty_module_missing_all_exports() { + let wat = "(module)"; + let engine = SharedEngine::new().unwrap(); + let result = WasmActorBuilder::new(engine, wat::parse_str(wat).unwrap()).build(); + assert!(result.is_err(), "module with no exports should fail to build"); + let err = result.err().unwrap(); + let err_msg = format!("{}", err); + assert!(err_msg.contains("memory") || err_msg.contains("alloc"), + "error should mention missing export: {err_msg}"); +} + +// Guest where alloc always returns the same pointer — repeated overwrites +#[test] +fn guest_static_alloc_same_pointer_echoes_each_msg() { + let engine = SharedEngine::new().unwrap(); + // Build a custom guest that always returns ptr 1024 from alloc + 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) + ;; Use first 32 bytes as dest, rest as payload + (if (i32.ge_u (local.get $len) (i32.const 33)) + (then + (call $send + (local.get $ptr) + (i32.add (local.get $ptr) (i32.const 32)) + (i32.sub (local.get $len) (i32.const 32)) + ) + ) + ) + ) + )"#; + 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(); + + // Each message goes to same ptr=1024, overwriting previous data + rt.send_to(addr, framed_msg(inbox.addr(), b"first")).unwrap(); + rt.send_to(addr, framed_msg(inbox.addr(), b"second")).unwrap(); + rt.tick(); + + let msgs: Vec> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); + assert_eq!(msgs.len(), 2, "both messages processed"); + // Since same pointer, second alloc overwrites first, but outbox snapshots data at send time + assert_eq!(msgs[0], b"first"); + assert_eq!(msgs[1], b"second"); +} + +// Multiple engines, multiple runtimes, cross-combination +#[test] +fn two_engines_two_runtimes_cross_combination() { + let engine_a = SharedEngine::new().unwrap(); + let engine_b = SharedEngine::new().unwrap(); + + let rt1 = Runtime::new(RuntimeConfig::default()); + let rt2 = Runtime::new(RuntimeConfig::default()); + + // Actor from engine_a on rt1 + let a1 = WasmActorBuilder::new(engine_a.clone(), guest_wasm("echo")).build().unwrap(); + // Actor from engine_b on rt2 + let a2 = WasmActorBuilder::new(engine_b.clone(), guest_wasm("echo")).build().unwrap(); + // Actor from engine_a on rt2 + let a3 = WasmActorBuilder::new(engine_a, guest_wasm("silent")).build().unwrap(); + // Actor from engine_b on rt1 + let a4 = WasmActorBuilder::new(engine_b, guest_wasm("double")).build().unwrap(); + + let inbox1 = rt1.new_inbox::().unwrap(); + let inbox2 = rt2.new_inbox::().unwrap(); + + let addr1 = rt1.spawn(a1).unwrap(); + let addr4 = rt1.spawn(a4).unwrap(); + let addr2 = rt2.spawn(a2).unwrap(); + let _addr3 = rt2.spawn(a3).unwrap(); + + rt1.send_to(addr1, framed_msg(inbox1.addr(), b"e1")).unwrap(); + rt1.send_to(addr4, framed_msg(inbox1.addr(), b"d1")).unwrap(); + rt2.send_to(addr2, framed_msg(inbox2.addr(), b"e2")).unwrap(); + + rt1.tick(); + rt2.tick(); + + let msgs1: Vec<_> = std::iter::from_fn(|| inbox1.try_recv().map(|m| m.0)).collect(); + let msgs2: Vec<_> = std::iter::from_fn(|| inbox2.try_recv().map(|m| m.0)).collect(); + + // rt1: echo(e1) + double(d1) = 1 + 2 = 3 + assert_eq!(msgs1.len(), 3); + // rt2: echo(e2) = 1 + assert_eq!(msgs2.len(), 1); + assert_eq!(msgs2[0], b"e2"); +} + +// 32KB payload stress test — echoes correctly +#[test] +fn thirty_two_kb_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(); + + let payload: Vec = (0..32768u32).map(|i| (i % 199) as u8).collect(); + rt.send_to(addr, framed_msg(inbox.addr(), &payload)).unwrap(); + rt.tick(); + + let resp = inbox.try_recv().expect("32KB echo"); + assert_eq!(resp.0.len(), 32768); + assert_eq!(resp.0, payload); } \ No newline at end of file