diff --git a/crates/wasm-actor/tests/wasm_actor.rs b/crates/wasm-actor/tests/wasm_actor.rs index 96fe7c6..042af85 100644 --- a/crates/wasm-actor/tests/wasm_actor.rs +++ b/crates/wasm-actor/tests/wasm_actor.rs @@ -13215,4 +13215,123 @@ fn hundred_kb_payload_dropped_for_one_page_guest() { rt.send_to(addr, framed_msg(inbox.addr(), b"alive")).unwrap(); rt.tick(); assert_eq!(inbox.try_recv().unwrap().0, b"alive"); +} + +// ── Cycle 92 ───────────────────────────────────────────────────────────────── + +// Guest that counts bytes equal to a threshold +#[test] +fn guest_counts_bytes_equal_to_threshold() { + 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 $count i32) + (if (i32.lt_u (local.get $len) (i32.const 34)) (then return)) + ;; Threshold is first byte after address (offset 32) + ;; Count occurrences in rest of payload + (local.set $i (i32.const 33)) + (local.set $count (i32.const 0)) + (block $exit + (loop $loop + (br_if $exit (i32.ge_u (local.get $i) (local.get $len))) + (if (i32.eq + (i32.load8_u (i32.add (local.get $ptr) (local.get $i))) + (i32.load8_u (i32.add (local.get $ptr) (i32.const 32)))) + (then (local.set $count (i32.add (local.get $count) (i32.const 1))))) + (local.set $i (i32.add (local.get $i) (i32.const 1))) + (br $loop) + ) + ) + ;; Send count as 4-byte LE at scratch 900 + (i32.store (i32.const 900) (local.get $count)) + (call $send (local.get $ptr) (i32.const 900) (i32.const 4)) + ) + )"#; + 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(); + + // Threshold = 0x42 (first byte), data to search = [0x00, 0x42, 0x42, 0xFF] + // Expected count = 2 (threshold byte itself not counted, search starts at offset 33) + rt.send_to(addr, framed_msg(inbox.addr(), &[0x42, 0x00, 0x42, 0x42, 0xFF])).unwrap(); + rt.tick(); + + let resp = inbox.try_recv().expect("count response"); + let count = u32::from_le_bytes([resp.0[0], resp.0[1], resp.0[2], resp.0[3]]); + assert_eq!(count, 2); +} + +// Spawn echo, stop it, spawn another echo at (likely) same slot +#[test] +fn reuse_slot_after_stop() { + let engine = SharedEngine::new().unwrap(); + let rt = Runtime::new(RuntimeConfig::default()); + let inbox = rt.new_inbox::().unwrap(); + + let a1 = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); + let addr1 = rt.spawn(a1).unwrap(); + rt.send_to(addr1, framed_msg(inbox.addr(), b"first")).unwrap(); + rt.tick(); + assert_eq!(inbox.try_recv().unwrap().0, b"first"); + rt.stop_actor(addr1); + rt.tick(); + + let a2 = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); + let addr2 = rt.spawn(a2).unwrap(); + rt.send_to(addr2, framed_msg(inbox.addr(), b"second")).unwrap(); + rt.tick(); + assert_eq!(inbox.try_recv().unwrap().0, b"second"); +} + +// Guest module with only memory and alloc — missing handle export +#[test] +fn module_missing_handle_export() { + let wat = r#"(module + (memory (export "memory") 1) + (func (export "alloc") (param $len i32) (result i32) i32.const 1024) + )"#; + let engine = SharedEngine::new().unwrap(); + let result = WasmActorBuilder::new(engine, wat::parse_str(wat).unwrap()).build(); + assert!(result.is_err()); + let err = result.err().unwrap(); + let msg = format!("{err}"); + assert!(msg.contains("handle"), "error should mention handle: {msg}"); +} + +// Property: silent actor never produces any inbox messages +proptest! { + #[test] + fn prop_silent_never_sends( + n_msgs in 1usize..20, + ticks in 1usize..10, + ) { + let engine = SharedEngine::new().unwrap(); + let actor = WasmActorBuilder::new(engine, guest_wasm("silent")).build().unwrap(); + let rt = Runtime::new(RuntimeConfig::default()); + let inbox = rt.new_inbox::().unwrap(); + let addr = rt.spawn(actor).unwrap(); + + for i in 0..n_msgs { + rt.send_to(addr, framed_msg(inbox.addr(), &[i as u8])).unwrap(); + } + for _ in 0..ticks { + rt.tick(); + } + + assert!(inbox.try_recv().is_none(), "silent actor should never send to inbox"); + } +} + +// Multiple ticks without any actors — no panics +#[test] +fn many_ticks_on_empty_runtime() { + let rt = Runtime::new(RuntimeConfig::default()); + for _ in 0..1000 { + rt.tick(); + } } \ No newline at end of file