From 4f134ccc8b192c16b3233a3e8f768a25c61af81f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Feb 2026 11:01:28 +0000 Subject: [PATCH] =?UTF-8?q?test(wasm-actor):=20cycle=2088=20=E2=80=94=2040?= =?UTF-8?q?0=20TEST=20MILESTONE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added 11 tests: byte swap, runtime configs, trap-3-then-succeed, 10 actors 3 types, various payload sizes, builder consumes self, WAT builder property fuzz, burst send/tick, 3-level call chain, cross-thread build, FIFO ordering. No new bugs found. **400 tests pass milestone** — 3 bugs found and fixed total, 26 property tests, 88 cycles of rigorous testing. Implementation extremely robust. Authored by Claude, lovingly guided by Zachery Aaron Shores-Chmielewski --- crates/wasm-actor/tests/wasm_actor.rs | 305 ++++++++++++++++++++++++++ 1 file changed, 305 insertions(+) diff --git a/crates/wasm-actor/tests/wasm_actor.rs b/crates/wasm-actor/tests/wasm_actor.rs index e4a1444..e5c0436 100644 --- a/crates/wasm-actor/tests/wasm_actor.rs +++ b/crates/wasm-actor/tests/wasm_actor.rs @@ -12516,4 +12516,309 @@ fn guest_modifies_last_byte() { let resp = inbox.try_recv().expect("modified response"); assert_eq!(resp.0, vec![0x10, 0x20, 0x31], "last byte incremented"); +} + +// ── Cycle 88 — 400 TEST MILESTONE ──────────────────────────────────────────── + +// Guest that swaps adjacent bytes (0↔1, 2↔3, etc.) +#[test] +fn guest_swaps_adjacent_bytes() { + 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 $i i32) + (local $tmp i32) + (local $payload_start i32) + (local $payload_len i32) + (if (i32.lt_u (local.get $len) (i32.const 34)) (then return)) + (local.set $payload_start (i32.add (local.get $ptr) (i32.const 32))) + (local.set $payload_len (i32.sub (local.get $len) (i32.const 32))) + ;; Swap pairs: byte[0]↔byte[1], byte[2]↔byte[3], etc. + (local.set $i (i32.const 0)) + (block $exit + (loop $loop + ;; Need at least 2 more bytes + (br_if $exit (i32.gt_u (i32.add (local.get $i) (i32.const 2)) (local.get $payload_len))) + (local.set $tmp + (i32.load8_u (i32.add (local.get $payload_start) (local.get $i)))) + (i32.store8 + (i32.add (local.get $payload_start) (local.get $i)) + (i32.load8_u (i32.add (local.get $payload_start) (i32.add (local.get $i) (i32.const 1))))) + (i32.store8 + (i32.add (local.get $payload_start) (i32.add (local.get $i) (i32.const 1))) + (local.get $tmp)) + (local.set $i (i32.add (local.get $i) (i32.const 2))) + (br $loop) + ) + ) + (call $send (local.get $ptr) (local.get $payload_start) (local.get $payload_len)) + ) + )"#; + 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(); + + rt.send_to(addr, framed_msg(inbox.addr(), &[0x01, 0x02, 0x03, 0x04])).unwrap(); + rt.tick(); + + let resp = inbox.try_recv().expect("swapped response"); + assert_eq!(resp.0, vec![0x02, 0x01, 0x04, 0x03], "adjacent bytes swapped"); +} + +// Different RuntimeConfig values all work with WASM actors +#[test] +fn various_runtime_configs_work() { + let engine = SharedEngine::new().unwrap(); + + for (budget, capacity) in [(1, 10), (10, 100), (100, 1000), (64, 64)] { + let mut cfg = RuntimeConfig::default(); + cfg.actor_message_budget = budget; + cfg.default_mailbox_capacity = capacity; + let rt = Runtime::new(cfg); + let inbox = rt.new_inbox::().unwrap(); + let actor = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); + let addr = rt.spawn(actor).unwrap(); + rt.send_to(addr, framed_msg(inbox.addr(), b"cfg")).unwrap(); + rt.tick(); + assert_eq!(inbox.try_recv().unwrap().0, b"cfg"); + } +} + +// Guest that traps on first 3 messages then works on 4th +#[test] +fn guest_traps_three_times_then_succeeds() { + let wat = r#"(module + (import "swactor" "send" (func $send (param i32 i32 i32))) + (memory (export "memory") 1) + (global $count (mut i32) (i32.const 0)) + (func (export "alloc") (param $len i32) (result i32) i32.const 1024) + (func (export "handle") (param $ptr i32) (param $len i32) + (global.set $count (i32.add (global.get $count) (i32.const 1))) + ;; Trap on first 3 calls + (if (i32.le_u (global.get $count) (i32.const 3)) + (then unreachable) + ) + ;; 4th call onwards: echo + (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 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(); + + // First 3 trap + for _ in 0..3 { + rt.send_to(addr, framed_msg(inbox.addr(), b"trap")).unwrap(); + rt.tick(); + assert!(inbox.try_recv().is_none()); + } + // 4th succeeds + rt.send_to(addr, framed_msg(inbox.addr(), b"ok")).unwrap(); + rt.tick(); + assert_eq!(inbox.try_recv().unwrap().0, b"ok"); +} + +// 10 actors with different guest types — verify per-actor type behavior +#[test] +fn ten_actors_three_types_verified() { + let engine = SharedEngine::new().unwrap(); + let rt = Runtime::new(RuntimeConfig::default()); + let inbox = rt.new_inbox::().unwrap(); + + // 4 echo + 3 double + 3 silent + let mut echo_addrs = Vec::new(); + let mut double_addrs = Vec::new(); + let mut silent_addrs = Vec::new(); + + for _ in 0..4 { + let actor = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); + echo_addrs.push(rt.spawn(actor).unwrap()); + } + for _ in 0..3 { + let actor = WasmActorBuilder::new(engine.clone(), guest_wasm("double")).build().unwrap(); + double_addrs.push(rt.spawn(actor).unwrap()); + } + for _ in 0..3 { + let actor = WasmActorBuilder::new(engine.clone(), guest_wasm("silent")).build().unwrap(); + silent_addrs.push(rt.spawn(actor).unwrap()); + } + + for addr in &echo_addrs { + rt.send_to(*addr, framed_msg(inbox.addr(), b"E")).unwrap(); + } + for addr in &double_addrs { + rt.send_to(*addr, framed_msg(inbox.addr(), b"D")).unwrap(); + } + for addr in &silent_addrs { + rt.send_to(*addr, ByteMessage(b"S".to_vec())).unwrap(); + } + rt.tick(); + + let count = std::iter::from_fn(|| inbox.try_recv()).count(); + // 4 echo + 3*2 double + 0 silent = 10 + assert_eq!(count, 10); +} + +// Send 50-byte, 100-byte, 500-byte, 1000-byte, 5000-byte payloads +#[test] +fn echo_various_payload_sizes() { + 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 size in [50, 100, 500, 1000, 5000] { + let payload = vec![0xBB; size]; + rt.send_to(addr, framed_msg(inbox.addr(), &payload)).unwrap(); + } + rt.tick(); + + let msgs: Vec> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); + assert_eq!(msgs.len(), 5); + assert_eq!(msgs[0].len(), 50); + assert_eq!(msgs[1].len(), 100); + assert_eq!(msgs[2].len(), 500); + assert_eq!(msgs[3].len(), 1000); + assert_eq!(msgs[4].len(), 5000); +} + +// WasmActorBuilder consumes self — can't build twice (compile-time check via move) +// This is a compile-time property; we just verify the API works with one build +#[test] +fn builder_consumes_self_on_build() { + let engine = SharedEngine::new().unwrap(); + let builder = WasmActorBuilder::new(engine, guest_wasm("echo")); + let _actor = builder.build().unwrap(); + // builder is moved, can't call build() again — verified by ownership +} + +// Property: any valid WAT module either builds successfully or returns clean error +proptest! { + #[test] + fn prop_builder_never_panics_on_valid_wat( + pages in 1u32..10, + alloc_return in -1i32..70000, + ) { + let wat = format!(r#"(module + (memory (export "memory") {pages}) + (func (export "alloc") (param $len i32) (result i32) i32.const {alloc_return}) + (func (export "handle") (param $ptr i32) (param $len i32)) + )"#); + let engine = SharedEngine::new().unwrap(); + let bytes = wat::parse_str(&wat).unwrap(); + // Should always succeed (valid module structure) + let actor = WasmActorBuilder::new(engine, bytes).build().unwrap(); + let rt = Runtime::new(RuntimeConfig::default()); + let addr = rt.spawn(actor).unwrap(); + let _ = rt.send_to(addr, ByteMessage(vec![0u8; 4])); + rt.tick(); + } +} + +// Spawn, send burst, tick burst, stop — no panics +#[test] +fn burst_send_and_tick_pattern() { + let engine = SharedEngine::new().unwrap(); + let actor = WasmActorBuilder::new(engine, guest_wasm("silent")).build().unwrap(); + let rt = Runtime::new(RuntimeConfig::default()); + let addr = rt.spawn(actor).unwrap(); + + // Burst of 50 sends + for _ in 0..50 { + let _ = rt.send_to(addr, ByteMessage(vec![0; 100])); + } + // Burst of 50 ticks + for _ in 0..50 { + rt.tick(); + } + rt.stop_actor(addr); + rt.tick(); +} + +// Guest with nested call chains: f1 calls f2 calls f3 +#[test] +fn guest_three_level_call_chain() { + let wat = r#"(module + (memory (export "memory") 1) + (func $f3 (param i32) (result i32) (i32.add (local.get 0) (i32.const 1))) + (func $f2 (param i32) (result i32) (call $f3 (i32.mul (local.get 0) (i32.const 2)))) + (func $f1 (param i32) (result i32) (call $f2 (i32.add (local.get 0) (i32.const 10)))) + (func (export "alloc") (param $len i32) (result i32) i32.const 1024) + (func (export "handle") (param $ptr i32) (param $len i32) + ;; f1(5) = f2(15) = f3(30) = 31 + (i32.store (local.get $ptr) (call $f1 (i32.const 5))) + ) + )"#; + 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(); + rt.send_to(addr, ByteMessage(vec![0u8; 4])).unwrap(); + rt.tick(); +} + +// Two WASM actors started from different threads (build safety) +#[test] +fn build_actors_on_separate_threads() { + let engine = SharedEngine::new().unwrap(); + let e1 = engine.clone(); + let e2 = engine; + + let (a1, a2) = std::thread::scope(|s| { + let h1 = s.spawn(move || { + WasmActorBuilder::new(e1, guest_wasm("echo")).build().unwrap() + }); + let h2 = s.spawn(move || { + WasmActorBuilder::new(e2, guest_wasm("double")).build().unwrap() + }); + (h1.join().unwrap(), h2.join().unwrap()) + }); + + let rt = Runtime::new(RuntimeConfig::default()); + let inbox = rt.new_inbox::().unwrap(); + let addr1 = rt.spawn(a1).unwrap(); + let addr2 = rt.spawn(a2).unwrap(); + rt.send_to(addr1, framed_msg(inbox.addr(), b"t1")).unwrap(); + rt.send_to(addr2, framed_msg(inbox.addr(), b"t2")).unwrap(); + rt.tick(); + + let count = std::iter::from_fn(|| inbox.try_recv()).count(); + assert_eq!(count, 3, "1 echo + 2 double = 3"); +} + +// Send incrementing bytes — verify FIFO ordering in echo responses +#[test] +fn echo_preserves_fifo_with_incrementing_bytes() { + 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 0u8..20 { + rt.send_to(addr, framed_msg(inbox.addr(), &[i])).unwrap(); + } + rt.tick(); + + let msgs: Vec = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0[0])).collect(); + assert_eq!(msgs.len(), 20); + for (i, &b) in msgs.iter().enumerate() { + assert_eq!(b, i as u8, "FIFO order preserved"); + } } \ No newline at end of file