test: Cycle 35 — loop sum compute, dest_ptr=0, stop-with-pending, truncated WAT fuzz

- handle_with_loop_computes_sum: iterative byte sum via wasm loop/br, u8 overflow wrapping
- send_with_dest_ptr_zero_reads_from_memory_start: dest_ptr=0 is a valid location
- stop_actor_with_pending_messages_no_crash: stop before tick with 10 queued messages
- prop_truncated_wat_always_produces_error: any truncated WASM bytes always fail to build

All 150 tests pass (10 property tests). No new bugs found.

Authored by Claude, lovingly guided by Zachery Aaron Shores-Chmielewski
This commit is contained in:
Claude 2026-02-13 09:42:02 +00:00
parent dfc671fd19
commit 68952517ea

View file

@ -5381,3 +5381,155 @@ fn handle_with_if_else_branching() {
assert_eq!(msg1.0, b"SML");
assert_eq!(msg2.0, b"BIG");
}
// ── Module with loop/br — iterative computation in handle ───────────────────
#[test]
fn handle_with_loop_computes_sum() {
// Guest sums all payload bytes using a loop and sends the sum as a single byte.
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 $i i32)
(local $sum i32)
(local $payload_start i32)
(local $payload_len i32)
;; payload starts at ptr+32, length is len-32
(local.set $payload_start (i32.add (local.get $ptr) (i32.const 32)))
(local.set $payload_len (i32.sub (local.get $len) (i32.const 32)))
(local.set $i (i32.const 0))
(local.set $sum (i32.const 0))
;; Sum loop
(block $break
(loop $loop
(br_if $break (i32.ge_u (local.get $i) (local.get $payload_len)))
(local.set $sum
(i32.add
(local.get $sum)
(i32.load8_u (i32.add (local.get $payload_start) (local.get $i)))
)
)
(local.set $i (i32.add (local.get $i) (i32.const 1)))
(br $loop)
)
)
;; Write sum (truncated to u8) at offset 200
(i32.store8 (i32.const 200) (local.get $sum))
(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::<ByteMessage>().unwrap();
let addr = rt.spawn(actor).unwrap();
// Sum of [1, 2, 3, 4, 5] = 15
rt.send_to(addr, framed_msg(inbox.addr(), &[1, 2, 3, 4, 5])).unwrap();
rt.tick();
let msg = inbox.try_recv().expect("should receive sum");
assert_eq!(msg.0[0], 15, "sum of [1,2,3,4,5] should be 15");
// Sum of [100, 100, 56] = 256 → truncated to 0 (u8 overflow)
rt.send_to(addr, framed_msg(inbox.addr(), &[100, 100, 56])).unwrap();
rt.tick();
let msg2 = inbox.try_recv().expect("should receive truncated sum");
assert_eq!(msg2.0[0], 0, "256 truncated to u8 wraps to 0");
}
// ── Send import with dest_ptr = 0 (valid, reads from start of memory) ──────
#[test]
fn send_with_dest_ptr_zero_reads_from_memory_start() {
// Guest copies the address to offset 0, then sends with dest_ptr=0.
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)
;; Copy 32-byte address from message to offset 0
(memory.copy (i32.const 0) (local.get $ptr) (i32.const 32))
;; Write payload at offset 300
(i32.store8 (i32.const 300) (i32.const 42))
;; Send with dest_ptr=0
(call $send (i32.const 0) (i32.const 300) (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::<ByteMessage>().unwrap();
let addr = rt.spawn(actor).unwrap();
rt.send_to(addr, framed_msg(inbox.addr(), b"zero-dest")).unwrap();
rt.tick();
let msg = inbox.try_recv().expect("dest_ptr=0 should be valid");
assert_eq!(msg.0, vec![42]);
}
// ── Wasm actors survive being stopped while message in flight ───────────────
#[test]
fn stop_actor_with_pending_messages_no_crash() {
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::<ByteMessage>().unwrap();
let addr = rt.spawn(actor).unwrap();
// Send messages, then stop before tick
for i in 0u8..10 {
rt.send_to(addr, framed_msg(inbox.addr(), &[i])).unwrap();
}
rt.stop_actor(addr);
rt.tick();
// The actor may or may not have processed some messages before being stopped.
// The important thing is no crash/panic.
let mut count = 0;
while let Some(_) = inbox.try_recv() {
count += 1;
}
// Count can be 0..=10, we just verify no panic
assert!(count <= 10, "at most 10 messages should be received");
}
// ── Property: build from any subset of valid WAT produces valid error ───────
proptest! {
#[test]
fn prop_truncated_wat_always_produces_error(
len in 0usize..200
) {
let full_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 0)
(func (export "handle") (param i32 i32))
)
"#;
let wasm_full = wat::parse_str(full_wat).unwrap();
// Truncate the WASM bytes
let truncated: Vec<u8> = wasm_full.iter().take(len).copied().collect();
if truncated.len() < wasm_full.len() {
let engine = SharedEngine::new().unwrap();
let result = WasmActorBuilder::new(engine, truncated).build();
// Truncated should always fail (unless we took all bytes)
assert!(result.is_err(), "truncated WASM should fail to build");
}
}
}