diff --git a/crates/wasm-actor/tests/wasm_actor.rs b/crates/wasm-actor/tests/wasm_actor.rs index 14f931e..bc54501 100644 --- a/crates/wasm-actor/tests/wasm_actor.rs +++ b/crates/wasm-actor/tests/wasm_actor.rs @@ -12220,4 +12220,133 @@ fn budget_leaves_remaining_for_next_tick() { assert_eq!(t1 + t2 + t3, 12, "all 12 processed across ticks"); assert!(t1 <= 5, "budget limits first tick"); +} + +// ── Cycle 86 ───────────────────────────────────────────────────────────────── + +// Guest doubles only specific payloads, echoes others +#[test] +fn guest_conditional_double_or_echo() { + 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 $payload_len i32) + (if (i32.lt_u (local.get $len) (i32.const 33)) (then return)) + (local.set $payload_len (i32.sub (local.get $len) (i32.const 32))) + ;; If first payload byte is 0xDD, double it + (if (i32.eq (i32.load8_u (i32.add (local.get $ptr) (i32.const 32))) (i32.const 0xDD)) + (then + ;; Send twice + (call $send (local.get $ptr) (i32.add (local.get $ptr) (i32.const 32)) (local.get $payload_len)) + (call $send (local.get $ptr) (i32.add (local.get $ptr) (i32.const 32)) (local.get $payload_len)) + ) + (else + ;; Echo once + (call $send (local.get $ptr) (i32.add (local.get $ptr) (i32.const 32)) (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(); + + // Regular message — echo + rt.send_to(addr, framed_msg(inbox.addr(), &[0xAA, 0xBB])).unwrap(); + // Trigger double + rt.send_to(addr, framed_msg(inbox.addr(), &[0xDD, 0xEE])).unwrap(); + rt.tick(); + + let msgs: Vec> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); + assert_eq!(msgs.len(), 3, "1 echo + 2 double = 3"); +} + +// Long-running WASM actor: 200 messages across 200 ticks +#[test] +fn two_hundred_messages_across_two_hundred_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(); + + for i in 0u8..200 { + rt.send_to(addr, framed_msg(inbox.addr(), &[i])).unwrap(); + rt.tick(); + let resp = inbox.try_recv().expect("each tick echoes"); + assert_eq!(resp.0, vec![i]); + } +} + +// Multiple inboxes from same runtime +#[test] +fn three_inboxes_from_same_runtime() { + let engine = SharedEngine::new().unwrap(); + let rt = Runtime::new(RuntimeConfig::default()); + let inbox1 = rt.new_inbox::().unwrap(); + let inbox2 = rt.new_inbox::().unwrap(); + let inbox3 = rt.new_inbox::().unwrap(); + + let actor = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); + let addr = rt.spawn(actor).unwrap(); + + // Send to echo targeting different inboxes + rt.send_to(addr, framed_msg(inbox1.addr(), b"one")).unwrap(); + rt.tick(); + rt.send_to(addr, framed_msg(inbox2.addr(), b"two")).unwrap(); + rt.tick(); + rt.send_to(addr, framed_msg(inbox3.addr(), b"three")).unwrap(); + rt.tick(); + + assert_eq!(inbox1.try_recv().unwrap().0, b"one"); + assert_eq!(inbox2.try_recv().unwrap().0, b"two"); + assert_eq!(inbox3.try_recv().unwrap().0, b"three"); +} + +// Guest that uses i32.and to mask bytes +#[test] +fn guest_and_mask_low_nibble() { + 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) + (if (i32.lt_u (local.get $len) (i32.const 33)) (then return)) + ;; Mask each payload byte to low nibble + (local.set $i (i32.const 32)) + (block $exit + (loop $loop + (br_if $exit (i32.ge_u (local.get $i) (local.get $len))) + (i32.store8 + (i32.add (local.get $ptr) (local.get $i)) + (i32.and + (i32.load8_u (i32.add (local.get $ptr) (local.get $i))) + (i32.const 0x0F))) + (local.set $i (i32.add (local.get $i) (i32.const 1))) + (br $loop) + ) + ) + (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(); + + rt.send_to(addr, framed_msg(inbox.addr(), &[0xAB, 0xCD, 0xEF])).unwrap(); + rt.tick(); + + let resp = inbox.try_recv().expect("masked response"); + assert_eq!(resp.0, vec![0x0B, 0x0D, 0x0F]); } \ No newline at end of file