Skip to main content

jugar_probar/mock/
wasm_runtime.rs

1//! Mock WASM Runtime for Testing Callback Patterns
2//!
3//! This module provides a mock runtime that simulates async message passing
4//! for WASM worker components without requiring browser APIs.
5//!
6//! Per `PROBAR-SPEC-WASM-001` Section 2.1.
7//!
8//! ## Browser Fidelity (PROBAR-WASM-003)
9//!
10//! To simulate real browser `structuredClone` semantics, messages are
11//! serialized and deserialized when passed through `receive_message`.
12//! This ensures that non-serializable types (like `Rc`, closures) will
13//! fail at test time, just as they would in a real browser.
14
15use serde::{Deserialize, Serialize};
16use std::cell::RefCell;
17use std::collections::VecDeque;
18use std::rc::Rc;
19
20/// Mock message types for testing worker communication
21///
22/// These mirror the actual message types used in WASM worker protocols.
23///
24/// ## Serialization Requirement (PROBAR-WASM-003)
25///
26/// All messages implement `Serialize` and `Deserialize` to simulate
27/// browser `structuredClone` semantics. Messages are round-tripped
28/// through serialization in `receive_message` to catch non-serializable
29/// payloads at test time.
30#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
31pub enum MockMessage {
32    /// Bootstrap message with base URL
33    Bootstrap {
34        /// Base URL for asset loading
35        base_url: String,
36    },
37    /// Initialization message with model URL
38    Init {
39        /// Model URL to load
40        model_url: String,
41    },
42    /// Worker ready signal
43    Ready,
44    /// Model loaded successfully
45    ModelLoaded {
46        /// Model size in MB
47        size_mb: f64,
48        /// Load time in milliseconds
49        load_time_ms: f64,
50    },
51    /// Start recording/processing
52    Start {
53        /// Sample rate in Hz
54        sample_rate: u32,
55    },
56    /// Stop recording/processing
57    Stop,
58    /// Partial result
59    Partial {
60        /// Partial text
61        text: String,
62        /// Whether this is the final result
63        is_final: bool,
64    },
65    /// Error occurred
66    Error {
67        /// Error message
68        message: String,
69    },
70    /// Shutdown request
71    Shutdown,
72    /// Custom message for extension
73    Custom {
74        /// Message type identifier
75        msg_type: String,
76        /// JSON payload
77        payload: String,
78    },
79}
80
81impl MockMessage {
82    /// Create a bootstrap message
83    #[must_use]
84    pub fn bootstrap(base_url: &str) -> Self {
85        Self::Bootstrap {
86            base_url: base_url.to_string(),
87        }
88    }
89
90    /// Create an init message
91    #[must_use]
92    pub fn init(model_url: &str) -> Self {
93        Self::Init {
94            model_url: model_url.to_string(),
95        }
96    }
97
98    /// Create a model loaded message
99    #[must_use]
100    pub fn model_loaded(size_mb: f64, load_time_ms: f64) -> Self {
101        Self::ModelLoaded {
102            size_mb,
103            load_time_ms,
104        }
105    }
106
107    /// Create a start message
108    #[must_use]
109    pub fn start(sample_rate: u32) -> Self {
110        Self::Start { sample_rate }
111    }
112
113    /// Create an error message
114    #[must_use]
115    pub fn error(message: &str) -> Self {
116        Self::Error {
117            message: message.to_string(),
118        }
119    }
120
121    /// Create a partial result message
122    #[must_use]
123    pub fn partial(text: &str, is_final: bool) -> Self {
124        Self::Partial {
125            text: text.to_string(),
126            is_final,
127        }
128    }
129}
130
131/// Mock runtime that simulates async message passing
132///
133/// This replaces browser APIs like `Worker.postMessage()` and `Worker.onmessage`
134/// with a deterministic, testable interface.
135pub struct MockWasmRuntime {
136    /// Incoming message queue (messages TO the component)
137    incoming: Rc<RefCell<VecDeque<MockMessage>>>,
138    /// Outgoing message queue (messages FROM the component)
139    outgoing: Rc<RefCell<VecDeque<MockMessage>>>,
140    /// Registered message handlers
141    handlers: Rc<RefCell<Vec<Box<dyn Fn(&MockMessage)>>>>,
142    /// Whether the runtime has been started
143    started: bool,
144    /// Total messages processed
145    messages_processed: usize,
146}
147
148impl Default for MockWasmRuntime {
149    fn default() -> Self {
150        Self::new()
151    }
152}
153
154impl std::fmt::Debug for MockWasmRuntime {
155    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156        f.debug_struct("MockWasmRuntime")
157            .field("incoming_count", &self.incoming.borrow().len())
158            .field("outgoing_count", &self.outgoing.borrow().len())
159            .field("handlers_count", &self.handlers.borrow().len())
160            .field("started", &self.started)
161            .field("messages_processed", &self.messages_processed)
162            .finish()
163    }
164}
165
166impl Clone for MockWasmRuntime {
167    fn clone(&self) -> Self {
168        Self {
169            incoming: Rc::clone(&self.incoming),
170            outgoing: Rc::clone(&self.outgoing),
171            handlers: Rc::clone(&self.handlers),
172            started: self.started,
173            messages_processed: self.messages_processed,
174        }
175    }
176}
177
178impl MockWasmRuntime {
179    /// Create a new mock runtime
180    #[must_use]
181    pub fn new() -> Self {
182        Self {
183            incoming: Rc::new(RefCell::new(VecDeque::new())),
184            outgoing: Rc::new(RefCell::new(VecDeque::new())),
185            handlers: Rc::new(RefCell::new(Vec::new())),
186            started: false,
187            messages_processed: 0,
188        }
189    }
190
191    /// Register a message handler (like `worker.onmessage`)
192    pub fn on_message<F>(&mut self, handler: F)
193    where
194        F: Fn(&MockMessage) + 'static,
195    {
196        self.handlers.borrow_mut().push(Box::new(handler));
197    }
198
199    /// Send message (like `worker.postMessage`)
200    ///
201    /// This puts a message in the outgoing queue for the component to "send".
202    ///
203    /// ## Browser Fidelity (PROBAR-WASM-003)
204    ///
205    /// Like `receive_message`, this performs a round-trip serialization to
206    /// simulate `structuredClone` semantics.
207    ///
208    /// # Panics
209    ///
210    /// Panics if the message cannot be serialized. This intentionally mirrors
211    /// browser `postMessage` semantics where non-cloneable objects throw.
212    #[allow(clippy::expect_used)] // Intentional: simulates browser postMessage failure
213    pub fn post_message(&self, msg: MockMessage) {
214        // Round-trip through bincode to simulate structuredClone
215        let serialized = bincode::serialize(&msg)
216            .expect("MockMessage serialization failed - this would fail in browser postMessage");
217        let cloned: MockMessage = bincode::deserialize(&serialized)
218            .expect("MockMessage deserialization failed - corrupted message");
219
220        self.outgoing.borrow_mut().push_back(cloned);
221    }
222
223    /// Receive a message (simulates worker sending to main thread)
224    ///
225    /// This puts a message in the incoming queue to be processed by handlers.
226    ///
227    /// ## Browser Fidelity (PROBAR-WASM-003)
228    ///
229    /// To simulate real browser `structuredClone` semantics, the message is
230    /// serialized and deserialized before being queued. This ensures that:
231    /// - Non-serializable types will panic (like they would in a browser)
232    /// - Message data is deep-copied (no shared references)
233    ///
234    /// # Panics
235    ///
236    /// Panics if the message cannot be serialized or deserialized. This
237    /// simulates browser `postMessage` behavior where non-cloneable objects
238    /// cause errors.
239    #[allow(clippy::expect_used)] // Intentional: simulates browser postMessage failure
240    pub fn receive_message(&self, msg: MockMessage) {
241        // Round-trip through bincode to simulate structuredClone
242        let serialized = bincode::serialize(&msg)
243            .expect("MockMessage serialization failed - this would fail in browser postMessage");
244        let cloned: MockMessage = bincode::deserialize(&serialized)
245            .expect("MockMessage deserialization failed - corrupted message");
246
247        self.incoming.borrow_mut().push_back(cloned);
248    }
249
250    /// Receive a message without serialization (bypass for testing)
251    ///
252    /// This is the legacy method that doesn't enforce serialization.
253    /// Use `receive_message` for browser-fidelity testing.
254    #[doc(hidden)]
255    pub fn receive_message_unchecked(&self, msg: MockMessage) {
256        self.incoming.borrow_mut().push_back(msg);
257    }
258
259    /// Process one message from the incoming queue
260    ///
261    /// Returns `true` if a message was processed, `false` if queue was empty.
262    ///
263    /// # Re-entrancy Safety
264    ///
265    /// Handlers may call `receive_message()` to queue additional messages,
266    /// or even register new handlers via `on_message()`. This is achieved
267    /// by temporarily swapping out the handlers vector during processing.
268    pub fn tick(&mut self) -> bool {
269        // Step 1: Pop message (borrow and release incoming)
270        let msg = self.incoming.borrow_mut().pop_front();
271
272        if let Some(msg) = msg {
273            // Helper guard to ensure handlers are restored even on panic
274            struct HandlersGuard {
275                handlers_ref: Rc<RefCell<Vec<Box<dyn Fn(&MockMessage)>>>>,
276                handlers_to_run: Vec<Box<dyn Fn(&MockMessage)>>,
277            }
278
279            impl Drop for HandlersGuard {
280                fn drop(&mut self) {
281                    let mut handlers = self.handlers_ref.borrow_mut();
282                    // Prepend original handlers (handlers_to_run), keeping new ones at the end
283                    let new_handlers = std::mem::take(&mut *handlers);
284                    *handlers = std::mem::take(&mut self.handlers_to_run);
285                    handlers.extend(new_handlers);
286                }
287            }
288
289            // Step 2: Swap out handlers using RAII guard for panic safety
290            let handlers_guard = HandlersGuard {
291                handlers_ref: Rc::clone(&self.handlers),
292                handlers_to_run: {
293                    let mut h = self.handlers.borrow_mut();
294                    std::mem::take(&mut *h)
295                },
296            };
297
298            // Step 3: Run all handlers with NO borrows held
299            for handler in &handlers_guard.handlers_to_run {
300                handler(&msg);
301            }
302
303            // Step 4 (Implicit): Guard drops here, restoring handlers via Drop trait
304
305            self.messages_processed += 1;
306            true
307        } else {
308            false
309        }
310    }
311
312    /// Process all pending messages
313    ///
314    /// # Safety Limit
315    ///
316    /// To prevent infinite loops from recursive message patterns,
317    /// this method processes at most 10,000 messages. Use `drain_bounded`
318    /// for explicit control over the limit.
319    pub fn drain(&mut self) {
320        self.drain_bounded(10_000);
321    }
322
323    /// Process pending messages with explicit bound
324    ///
325    /// Returns the number of messages processed.
326    pub fn drain_bounded(&mut self, max_messages: usize) -> usize {
327        let mut processed = 0;
328        while processed < max_messages && self.tick() {
329            processed += 1;
330        }
331        processed
332    }
333
334    /// Process up to N messages
335    pub fn tick_n(&mut self, n: usize) -> usize {
336        let mut processed = 0;
337        for _ in 0..n {
338            if self.tick() {
339                processed += 1;
340            } else {
341                break;
342            }
343        }
344        processed
345    }
346
347    /// Get pending incoming message count
348    #[must_use]
349    pub fn pending_count(&self) -> usize {
350        self.incoming.borrow().len()
351    }
352
353    /// Get outgoing messages (for assertions)
354    #[must_use]
355    pub fn take_outgoing(&self) -> Vec<MockMessage> {
356        self.outgoing.borrow_mut().drain(..).collect()
357    }
358
359    /// Peek at outgoing messages without consuming
360    #[must_use]
361    pub fn peek_outgoing(&self) -> Vec<MockMessage> {
362        self.outgoing.borrow().iter().cloned().collect()
363    }
364
365    /// Check if there are any outgoing messages
366    #[must_use]
367    pub fn has_outgoing(&self) -> bool {
368        !self.outgoing.borrow().is_empty()
369    }
370
371    /// Get total messages processed
372    #[must_use]
373    pub fn total_processed(&self) -> usize {
374        self.messages_processed
375    }
376
377    /// Clear all queues and handlers
378    pub fn reset(&mut self) {
379        self.incoming.borrow_mut().clear();
380        self.outgoing.borrow_mut().clear();
381        self.handlers.borrow_mut().clear();
382        self.messages_processed = 0;
383    }
384
385    /// Start the runtime (marks it as active)
386    pub fn start(&mut self) {
387        self.started = true;
388    }
389
390    /// Check if runtime is started
391    #[must_use]
392    pub fn is_started(&self) -> bool {
393        self.started
394    }
395}
396
397/// Trait for WASM components that can be tested with mock runtime
398///
399/// Components implement this trait to enable testing with `WasmCallbackTestHarness`.
400pub trait MockableWorker: Sized {
401    /// Create the worker with a mock runtime instead of real browser APIs
402    fn with_mock_runtime(runtime: MockWasmRuntime) -> Self;
403
404    /// Get the current state as a string (for assertions)
405    fn get_state(&self) -> String;
406
407    /// Get internal state for debugging (may differ from public state in buggy code)
408    ///
409    /// If this differs from `get_state()`, there's a state sync bug!
410    fn debug_internal_state(&self) -> String {
411        self.get_state() // Default implementation assumes no desync
412    }
413
414    /// Check for state synchronization
415    ///
416    /// Returns `true` if reported state matches internal state.
417    fn is_state_synced(&self) -> bool {
418        self.get_state() == self.debug_internal_state()
419    }
420}
421
422#[cfg(test)]
423#[allow(clippy::unwrap_used, clippy::expect_used)]
424mod tests {
425    use super::*;
426
427    #[test]
428    fn test_mock_message_constructors() {
429        let bootstrap = MockMessage::bootstrap("http://localhost:8080");
430        assert!(matches!(
431            bootstrap,
432            MockMessage::Bootstrap { base_url } if base_url == "http://localhost:8080"
433        ));
434
435        let init = MockMessage::init("/models/whisper-tiny.apr");
436        assert!(
437            matches!(init, MockMessage::Init { model_url } if model_url == "/models/whisper-tiny.apr")
438        );
439
440        let loaded = MockMessage::model_loaded(39.0, 1500.0);
441        assert!(matches!(
442            loaded,
443            MockMessage::ModelLoaded { size_mb, load_time_ms }
444            if (size_mb - 39.0).abs() < f64::EPSILON && (load_time_ms - 1500.0).abs() < f64::EPSILON
445        ));
446
447        let start = MockMessage::start(48000);
448        assert!(matches!(start, MockMessage::Start { sample_rate } if sample_rate == 48000));
449
450        let error = MockMessage::error("Test error");
451        assert!(matches!(error, MockMessage::Error { message } if message == "Test error"));
452
453        let partial = MockMessage::partial("Hello", false);
454        assert!(
455            matches!(partial, MockMessage::Partial { text, is_final } if text == "Hello" && !is_final)
456        );
457    }
458
459    #[test]
460    fn test_mock_runtime_message_flow() {
461        let mut runtime = MockWasmRuntime::new();
462        let received = Rc::new(RefCell::new(Vec::new()));
463        let received_clone = Rc::clone(&received);
464
465        runtime.on_message(move |msg| {
466            received_clone.borrow_mut().push(msg.clone());
467        });
468
469        // Receive messages
470        runtime.receive_message(MockMessage::Ready);
471        runtime.receive_message(MockMessage::model_loaded(39.0, 1500.0));
472
473        assert_eq!(runtime.pending_count(), 2);
474
475        // Process one
476        assert!(runtime.tick());
477        assert_eq!(received.borrow().len(), 1);
478        assert!(matches!(&received.borrow()[0], MockMessage::Ready));
479
480        // Process remaining
481        runtime.drain();
482        assert_eq!(received.borrow().len(), 2);
483        assert_eq!(runtime.total_processed(), 2);
484    }
485
486    #[test]
487    fn test_mock_runtime_outgoing() {
488        let runtime = MockWasmRuntime::new();
489
490        runtime.post_message(MockMessage::start(48000));
491        runtime.post_message(MockMessage::Stop);
492
493        assert!(runtime.has_outgoing());
494        assert_eq!(runtime.peek_outgoing().len(), 2);
495
496        let outgoing = runtime.take_outgoing();
497        assert_eq!(outgoing.len(), 2);
498        assert!(!runtime.has_outgoing());
499    }
500
501    #[test]
502    fn test_mock_runtime_clone() {
503        let runtime1 = MockWasmRuntime::new();
504        runtime1.receive_message(MockMessage::Ready);
505
506        let runtime2 = runtime1;
507
508        // Both share the same queues
509        assert_eq!(runtime2.pending_count(), 1);
510    }
511
512    #[test]
513    fn test_mock_runtime_tick_n() {
514        let mut runtime = MockWasmRuntime::new();
515        let count = Rc::new(RefCell::new(0));
516        let count_clone = Rc::clone(&count);
517
518        runtime.on_message(move |_| {
519            *count_clone.borrow_mut() += 1;
520        });
521
522        for _ in 0..10 {
523            runtime.receive_message(MockMessage::Ready);
524        }
525
526        // Process only 5
527        let processed = runtime.tick_n(5);
528        assert_eq!(processed, 5);
529        assert_eq!(*count.borrow(), 5);
530        assert_eq!(runtime.pending_count(), 5);
531    }
532
533    #[test]
534    fn test_mock_runtime_reset() {
535        let mut runtime = MockWasmRuntime::new();
536
537        runtime.receive_message(MockMessage::Ready);
538        runtime.post_message(MockMessage::Stop);
539        runtime.on_message(|_| {});
540        runtime.tick();
541
542        assert!(runtime.total_processed() > 0);
543
544        runtime.reset();
545
546        assert_eq!(runtime.pending_count(), 0);
547        assert!(!runtime.has_outgoing());
548        assert_eq!(runtime.total_processed(), 0);
549    }
550
551    #[test]
552    fn test_mock_message_equality() {
553        let msg1 = MockMessage::model_loaded(39.0, 1500.0);
554        let msg2 = MockMessage::model_loaded(39.0, 1500.0);
555        let msg3 = MockMessage::model_loaded(40.0, 1500.0);
556
557        assert_eq!(msg1, msg2);
558        assert_ne!(msg1, msg3);
559    }
560
561    #[test]
562    fn test_mock_runtime_default() {
563        let runtime = MockWasmRuntime::default();
564        assert!(!runtime.is_started());
565        assert_eq!(runtime.pending_count(), 0);
566        assert_eq!(runtime.total_processed(), 0);
567    }
568
569    #[test]
570    fn test_mock_runtime_debug() {
571        let runtime = MockWasmRuntime::new();
572        let debug_str = format!("{:?}", runtime);
573        assert!(debug_str.contains("MockWasmRuntime"));
574        assert!(debug_str.contains("incoming_count"));
575        assert!(debug_str.contains("started"));
576    }
577
578    #[test]
579    fn test_mock_runtime_start() {
580        let mut runtime = MockWasmRuntime::new();
581        assert!(!runtime.is_started());
582        runtime.start();
583        assert!(runtime.is_started());
584    }
585
586    #[test]
587    fn test_mock_runtime_receive_message_unchecked() {
588        let runtime = MockWasmRuntime::new();
589        runtime.receive_message_unchecked(MockMessage::Ready);
590        assert_eq!(runtime.pending_count(), 1);
591    }
592
593    #[test]
594    fn test_mock_runtime_tick_empty() {
595        let mut runtime = MockWasmRuntime::new();
596        assert!(!runtime.tick());
597        assert_eq!(runtime.total_processed(), 0);
598    }
599
600    #[test]
601    fn test_mock_runtime_drain_bounded() {
602        let mut runtime = MockWasmRuntime::new();
603        let counter = Rc::new(RefCell::new(0));
604        let counter_clone = Rc::clone(&counter);
605
606        runtime.on_message(move |_| {
607            *counter_clone.borrow_mut() += 1;
608        });
609
610        for _ in 0..20 {
611            runtime.receive_message(MockMessage::Ready);
612        }
613
614        // Process only 5
615        let processed = runtime.drain_bounded(5);
616        assert_eq!(processed, 5);
617        assert_eq!(*counter.borrow(), 5);
618        assert_eq!(runtime.pending_count(), 15);
619    }
620
621    #[test]
622    fn test_mock_runtime_drain_all() {
623        let mut runtime = MockWasmRuntime::new();
624        for _ in 0..10 {
625            runtime.receive_message(MockMessage::Ready);
626        }
627
628        runtime.drain();
629        assert_eq!(runtime.pending_count(), 0);
630    }
631
632    #[test]
633    fn test_mock_runtime_clone_shared_state() {
634        let runtime1 = MockWasmRuntime::new();
635        let runtime2 = runtime1.clone();
636
637        runtime1.receive_message(MockMessage::Ready);
638        // Both should see the same pending message
639        assert_eq!(runtime1.pending_count(), 1);
640        assert_eq!(runtime2.pending_count(), 1);
641
642        runtime2.post_message(MockMessage::Stop);
643        assert!(runtime1.has_outgoing());
644        assert!(runtime2.has_outgoing());
645    }
646
647    #[test]
648    fn test_mock_runtime_peek_outgoing() {
649        let runtime = MockWasmRuntime::new();
650        runtime.post_message(MockMessage::start(48000));
651        runtime.post_message(MockMessage::Stop);
652
653        let peeked = runtime.peek_outgoing();
654        assert_eq!(peeked.len(), 2);
655
656        // peek_outgoing doesn't consume
657        let peeked_again = runtime.peek_outgoing();
658        assert_eq!(peeked_again.len(), 2);
659    }
660
661    #[test]
662    fn test_mock_runtime_take_outgoing_consumes() {
663        let runtime = MockWasmRuntime::new();
664        runtime.post_message(MockMessage::Ready);
665
666        let taken = runtime.take_outgoing();
667        assert_eq!(taken.len(), 1);
668
669        // Should be empty after take
670        assert!(!runtime.has_outgoing());
671        let taken_again = runtime.take_outgoing();
672        assert!(taken_again.is_empty());
673    }
674
675    #[test]
676    fn test_mock_message_custom() {
677        let msg = MockMessage::Custom {
678            msg_type: "test".to_string(),
679            payload: r#"{"key": "value"}"#.to_string(),
680        };
681
682        match msg {
683            MockMessage::Custom { msg_type, payload } => {
684                assert_eq!(msg_type, "test");
685                assert!(payload.contains("key"));
686            }
687            _ => panic!("Expected Custom message"),
688        }
689    }
690
691    #[test]
692    fn test_mock_message_partial() {
693        let msg = MockMessage::partial("Hello world", true);
694        match msg {
695            MockMessage::Partial { text, is_final } => {
696                assert_eq!(text, "Hello world");
697                assert!(is_final);
698            }
699            _ => panic!("Expected Partial message"),
700        }
701
702        let msg2 = MockMessage::partial("Partial", false);
703        match msg2 {
704            MockMessage::Partial { is_final, .. } => {
705                assert!(!is_final);
706            }
707            _ => panic!("Expected Partial message"),
708        }
709    }
710
711    #[test]
712    fn test_mock_message_serialization() {
713        // Test all message variants can be serialized/deserialized
714        let messages = vec![
715            MockMessage::bootstrap("http://localhost"),
716            MockMessage::init("/model.apr"),
717            MockMessage::Ready,
718            MockMessage::model_loaded(100.0, 2000.0),
719            MockMessage::start(44100),
720            MockMessage::Stop,
721            MockMessage::partial("text", true),
722            MockMessage::error("oops"),
723            MockMessage::Shutdown,
724            MockMessage::Custom {
725                msg_type: "t".into(),
726                payload: "{}".into(),
727            },
728        ];
729
730        for msg in messages {
731            let serialized = bincode::serialize(&msg).expect("Should serialize");
732            let deserialized: MockMessage =
733                bincode::deserialize(&serialized).expect("Should deserialize");
734            assert_eq!(msg, deserialized);
735        }
736    }
737
738    #[test]
739    fn test_mockable_worker_is_state_synced() {
740        struct TestWorker {
741            reported: String,
742            internal: String,
743        }
744
745        impl MockableWorker for TestWorker {
746            fn with_mock_runtime(_: MockWasmRuntime) -> Self {
747                Self {
748                    reported: "same".into(),
749                    internal: "same".into(),
750                }
751            }
752
753            fn get_state(&self) -> String {
754                self.reported.clone()
755            }
756
757            fn debug_internal_state(&self) -> String {
758                self.internal.clone()
759            }
760        }
761
762        let worker = TestWorker {
763            reported: "state".into(),
764            internal: "state".into(),
765        };
766        assert!(worker.is_state_synced());
767
768        let desynced = TestWorker {
769            reported: "one".into(),
770            internal: "two".into(),
771        };
772        assert!(!desynced.is_state_synced());
773    }
774
775    #[test]
776    fn test_mock_runtime_multiple_handlers() {
777        let mut runtime = MockWasmRuntime::new();
778        let counter1 = Rc::new(RefCell::new(0));
779        let counter2 = Rc::new(RefCell::new(0));
780
781        let c1 = Rc::clone(&counter1);
782        runtime.on_message(move |_| {
783            *c1.borrow_mut() += 1;
784        });
785
786        let c2 = Rc::clone(&counter2);
787        runtime.on_message(move |_| {
788            *c2.borrow_mut() += 10;
789        });
790
791        runtime.receive_message(MockMessage::Ready);
792        runtime.tick();
793
794        // Both handlers should have been called
795        assert_eq!(*counter1.borrow(), 1);
796        assert_eq!(*counter2.borrow(), 10);
797    }
798
799    #[test]
800    fn test_mock_runtime_handler_adds_new_handler() {
801        let mut runtime = MockWasmRuntime::new();
802        let counter = Rc::new(RefCell::new(0));
803        let counter_clone = Rc::clone(&counter);
804
805        // Handler that uses counter
806        runtime.on_message(move |_| {
807            *counter_clone.borrow_mut() += 1;
808        });
809
810        // Process first message
811        runtime.receive_message(MockMessage::Ready);
812        runtime.tick();
813        assert_eq!(*counter.borrow(), 1);
814
815        // Process second message
816        runtime.receive_message(MockMessage::Stop);
817        runtime.tick();
818        assert_eq!(*counter.borrow(), 2);
819    }
820
821    #[test]
822    fn test_mock_runtime_tick_n_partial() {
823        let mut runtime = MockWasmRuntime::new();
824
825        // Add 3 messages
826        runtime.receive_message(MockMessage::Ready);
827        runtime.receive_message(MockMessage::Stop);
828        runtime.receive_message(MockMessage::Shutdown);
829
830        // Process only 2
831        let processed = runtime.tick_n(2);
832        assert_eq!(processed, 2);
833        assert_eq!(runtime.pending_count(), 1);
834    }
835
836    #[test]
837    fn test_mock_runtime_tick_n_more_than_available() {
838        let mut runtime = MockWasmRuntime::new();
839        runtime.receive_message(MockMessage::Ready);
840
841        // Try to process 100, but only 1 is available
842        let processed = runtime.tick_n(100);
843        assert_eq!(processed, 1);
844        assert_eq!(runtime.pending_count(), 0);
845    }
846
847    #[test]
848    fn test_mock_runtime_tick_n_zero() {
849        let mut runtime = MockWasmRuntime::new();
850        runtime.receive_message(MockMessage::Ready);
851
852        let processed = runtime.tick_n(0);
853        assert_eq!(processed, 0);
854        assert_eq!(runtime.pending_count(), 1);
855    }
856}