Skip to main content

jugar_probar/runner/
server.rs

1//! WASM Development Server
2//!
3//! HTTP server with hot reload support for WASM development.
4
5use super::config::WasmRunnerConfig;
6use serde::{Deserialize, Serialize};
7use std::time::Duration;
8
9/// Debug output configuration
10#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
11pub struct DebugOutput {
12    /// Show console.log from WASM
13    pub console: bool,
14    /// Show performance metrics
15    pub metrics: bool,
16    /// Show network requests
17    pub network: bool,
18    /// Show WASM memory usage
19    pub memory: bool,
20}
21
22impl Default for DebugOutput {
23    fn default() -> Self {
24        Self {
25            console: true,
26            metrics: true,
27            network: false,
28            memory: false,
29        }
30    }
31}
32
33impl DebugOutput {
34    /// Create with all output disabled
35    #[must_use]
36    pub fn none() -> Self {
37        Self {
38            console: false,
39            metrics: false,
40            network: false,
41            memory: false,
42        }
43    }
44
45    /// Create with all output enabled
46    #[must_use]
47    pub fn all() -> Self {
48        Self {
49            console: true,
50            metrics: true,
51            network: true,
52            memory: true,
53        }
54    }
55
56    /// Enable console output
57    #[must_use]
58    pub fn with_console(mut self) -> Self {
59        self.console = true;
60        self
61    }
62
63    /// Enable metrics output
64    #[must_use]
65    pub fn with_metrics(mut self) -> Self {
66        self.metrics = true;
67        self
68    }
69
70    /// Enable network output
71    #[must_use]
72    pub fn with_network(mut self) -> Self {
73        self.network = true;
74        self
75    }
76
77    /// Enable memory output
78    #[must_use]
79    pub fn with_memory(mut self) -> Self {
80        self.memory = true;
81        self
82    }
83}
84
85/// Hot reload event
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub enum HotReloadEvent {
88    /// File changed, triggering rebuild
89    FileChanged {
90        /// Path to changed file
91        path: String,
92    },
93    /// Rebuild started
94    RebuildStarted,
95    /// Rebuild completed successfully
96    Rebuild {
97        /// Build duration
98        duration: Duration,
99        /// State types preserved
100        preserved: Vec<String>,
101    },
102    /// Rebuild failed
103    RebuildFailed {
104        /// Error messages
105        errors: Vec<String>,
106    },
107    /// Client connected
108    ClientConnected {
109        /// Client ID
110        id: u32,
111    },
112    /// Client disconnected
113    ClientDisconnected {
114        /// Client ID
115        id: u32,
116    },
117}
118
119/// Console message from WASM
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct ConsoleMessage {
122    /// Log level
123    pub level: ConsoleLevel,
124    /// Message text
125    pub text: String,
126    /// Timestamp
127    pub timestamp: std::time::SystemTime,
128}
129
130/// Console log level
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
132pub enum ConsoleLevel {
133    /// Debug level
134    Debug,
135    /// Info level
136    Info,
137    /// Log level
138    Log,
139    /// Warning level
140    Warn,
141    /// Error level
142    Error,
143}
144
145impl ConsoleLevel {
146    /// Get the prefix for terminal output
147    #[must_use]
148    pub fn prefix(&self) -> &'static str {
149        match self {
150            ConsoleLevel::Debug => "\x1b[34m[DEBUG]\x1b[0m",
151            ConsoleLevel::Info => "\x1b[36m[INFO]\x1b[0m",
152            ConsoleLevel::Log => "[LOG]",
153            ConsoleLevel::Warn => "\x1b[33m[WARN]\x1b[0m",
154            ConsoleLevel::Error => "\x1b[31m[ERROR]\x1b[0m",
155        }
156    }
157}
158
159/// WASM development server runner
160#[derive(Debug)]
161pub struct WasmRunner {
162    config: WasmRunnerConfig,
163    debug_output: DebugOutput,
164    running: bool,
165    clients: Vec<u32>,
166    next_client_id: u32,
167}
168
169impl WasmRunner {
170    /// Create a new runner with configuration
171    #[must_use]
172    pub fn new(config: WasmRunnerConfig) -> Self {
173        Self {
174            config,
175            debug_output: DebugOutput::default(),
176            running: false,
177            clients: Vec::new(),
178            next_client_id: 1,
179        }
180    }
181
182    /// Create a builder
183    #[must_use]
184    pub fn builder() -> WasmRunnerBuilder {
185        WasmRunnerBuilder::default()
186    }
187
188    /// Get configuration
189    #[must_use]
190    pub fn config(&self) -> &WasmRunnerConfig {
191        &self.config
192    }
193
194    /// Get debug output configuration
195    #[must_use]
196    pub fn debug_output(&self) -> &DebugOutput {
197        &self.debug_output
198    }
199
200    /// Set debug output configuration
201    pub fn set_debug_output(&mut self, output: DebugOutput) {
202        self.debug_output = output;
203    }
204
205    /// Check if server is running
206    #[must_use]
207    pub fn is_running(&self) -> bool {
208        self.running
209    }
210
211    /// Get connected client count
212    #[must_use]
213    pub fn client_count(&self) -> usize {
214        self.clients.len()
215    }
216
217    /// Simulate starting the server (for testing)
218    pub fn simulate_start(&mut self) {
219        self.running = true;
220    }
221
222    /// Simulate stopping the server (for testing)
223    pub fn simulate_stop(&mut self) {
224        self.running = false;
225        self.clients.clear();
226    }
227
228    /// Simulate client connection (for testing)
229    pub fn simulate_client_connect(&mut self) -> u32 {
230        let id = self.next_client_id;
231        self.next_client_id += 1;
232        self.clients.push(id);
233        id
234    }
235
236    /// Simulate client disconnection (for testing)
237    pub fn simulate_client_disconnect(&mut self, id: u32) {
238        self.clients.retain(|&c| c != id);
239    }
240
241    /// Format a console message for terminal output
242    #[must_use]
243    pub fn format_console_message(&self, msg: &ConsoleMessage) -> String {
244        // Platform-independent timestamp formatting
245        #[cfg(not(target_arch = "wasm32"))]
246        {
247            use chrono::{DateTime, Local};
248            let timestamp: DateTime<Local> = msg.timestamp.into();
249            format!(
250                "[{}] {} {}",
251                timestamp.format("%H:%M:%S"),
252                msg.level.prefix(),
253                msg.text
254            )
255        }
256        #[cfg(target_arch = "wasm32")]
257        {
258            // On WASM, use epoch seconds as timestamp
259            let secs = msg
260                .timestamp
261                .duration_since(std::time::UNIX_EPOCH)
262                .map(|d| d.as_secs())
263                .unwrap_or(0);
264            format!("[{}] {} {}", secs, msg.level.prefix(), msg.text)
265        }
266    }
267
268    /// Get server URL
269    #[must_use]
270    pub fn http_url(&self) -> String {
271        format!("http://localhost:{}", self.config.http_port)
272    }
273
274    /// Get WebSocket URL
275    #[must_use]
276    pub fn ws_url(&self) -> String {
277        format!("ws://localhost:{}", self.config.ws_port)
278    }
279}
280
281/// Builder for `WasmRunner`
282#[derive(Debug, Clone, Default)]
283pub struct WasmRunnerBuilder {
284    config: WasmRunnerConfig,
285    debug_output: DebugOutput,
286}
287
288impl WasmRunnerBuilder {
289    /// Create a new builder
290    #[must_use]
291    pub fn new() -> Self {
292        Self::default()
293    }
294
295    /// Set HTTP port
296    #[must_use]
297    pub fn http_port(mut self, port: u16) -> Self {
298        self.config.http_port = port;
299        self
300    }
301
302    /// Set WebSocket port
303    #[must_use]
304    pub fn ws_port(mut self, port: u16) -> Self {
305        self.config.ws_port = port;
306        self
307    }
308
309    /// Enable/disable hot reload
310    #[must_use]
311    pub fn hot_reload(mut self, enabled: bool) -> Self {
312        self.config.hot_reload = enabled;
313        self
314    }
315
316    /// Enable/disable state preservation
317    #[must_use]
318    pub fn preserve_state(mut self, enabled: bool) -> Self {
319        self.config.preserve_state = enabled;
320        self
321    }
322
323    /// Enable/disable source maps
324    #[must_use]
325    pub fn source_maps(mut self, enabled: bool) -> Self {
326        self.config.source_maps = enabled;
327        self
328    }
329
330    /// Set debug output configuration
331    #[must_use]
332    pub fn debug_output(mut self, output: DebugOutput) -> Self {
333        self.debug_output = output;
334        self
335    }
336
337    /// Build the runner
338    #[must_use]
339    pub fn build(self) -> WasmRunner {
340        let mut runner = WasmRunner::new(self.config);
341        runner.debug_output = self.debug_output;
342        runner
343    }
344}
345
346#[cfg(test)]
347#[allow(clippy::unwrap_used, clippy::expect_used)]
348mod tests {
349    use super::*;
350
351    #[test]
352    fn test_debug_output_default() {
353        let output = DebugOutput::default();
354        assert!(output.console);
355        assert!(output.metrics);
356        assert!(!output.network);
357        assert!(!output.memory);
358    }
359
360    #[test]
361    fn test_debug_output_none() {
362        let output = DebugOutput::none();
363        assert!(!output.console);
364        assert!(!output.metrics);
365        assert!(!output.network);
366        assert!(!output.memory);
367    }
368
369    #[test]
370    fn test_debug_output_all() {
371        let output = DebugOutput::all();
372        assert!(output.console);
373        assert!(output.metrics);
374        assert!(output.network);
375        assert!(output.memory);
376    }
377
378    #[test]
379    fn test_debug_output_builders() {
380        let output = DebugOutput::none().with_console().with_network();
381
382        assert!(output.console);
383        assert!(!output.metrics);
384        assert!(output.network);
385        assert!(!output.memory);
386    }
387
388    #[test]
389    fn test_console_level_prefix() {
390        assert!(ConsoleLevel::Error.prefix().contains("[ERROR]"));
391        assert!(ConsoleLevel::Warn.prefix().contains("[WARN]"));
392        assert!(ConsoleLevel::Info.prefix().contains("[INFO]"));
393    }
394
395    #[test]
396    fn test_wasm_runner_new() {
397        let config = WasmRunnerConfig::default();
398        let runner = WasmRunner::new(config);
399        assert!(!runner.is_running());
400        assert_eq!(runner.client_count(), 0);
401    }
402
403    #[test]
404    fn test_wasm_runner_builder() {
405        let runner = WasmRunnerBuilder::new()
406            .http_port(9000)
407            .ws_port(9001)
408            .hot_reload(false)
409            .build();
410
411        assert_eq!(runner.config().http_port, 9000);
412        assert_eq!(runner.config().ws_port, 9001);
413        assert!(!runner.config().hot_reload);
414    }
415
416    #[test]
417    fn test_wasm_runner_urls() {
418        let runner = WasmRunnerBuilder::new()
419            .http_port(8080)
420            .ws_port(8081)
421            .build();
422
423        assert_eq!(runner.http_url(), "http://localhost:8080");
424        assert_eq!(runner.ws_url(), "ws://localhost:8081");
425    }
426
427    #[test]
428    fn test_wasm_runner_simulate() {
429        let mut runner = WasmRunner::new(WasmRunnerConfig::default());
430
431        runner.simulate_start();
432        assert!(runner.is_running());
433
434        let id1 = runner.simulate_client_connect();
435        let id2 = runner.simulate_client_connect();
436        assert_eq!(runner.client_count(), 2);
437
438        runner.simulate_client_disconnect(id1);
439        assert_eq!(runner.client_count(), 1);
440
441        runner.simulate_stop();
442        assert!(!runner.is_running());
443        assert_eq!(runner.client_count(), 0);
444
445        // Ensure IDs are unique
446        assert_ne!(id1, id2);
447    }
448
449    #[test]
450    fn test_hot_reload_event_variants() {
451        let event = HotReloadEvent::FileChanged {
452            path: "src/main.rs".to_string(),
453        };
454        assert!(matches!(event, HotReloadEvent::FileChanged { .. }));
455
456        let event = HotReloadEvent::Rebuild {
457            duration: Duration::from_millis(500),
458            preserved: vec!["State".to_string()],
459        };
460        assert!(matches!(event, HotReloadEvent::Rebuild { .. }));
461    }
462
463    #[test]
464    fn test_console_message() {
465        let msg = ConsoleMessage {
466            level: ConsoleLevel::Info,
467            text: "Test message".to_string(),
468            timestamp: std::time::SystemTime::now(),
469        };
470
471        let runner = WasmRunner::new(WasmRunnerConfig::default());
472        let formatted = runner.format_console_message(&msg);
473        assert!(formatted.contains("[INFO]"));
474        assert!(formatted.contains("Test message"));
475    }
476
477    #[test]
478    fn test_debug_output_with_metrics() {
479        let output = DebugOutput::none().with_metrics();
480        assert!(!output.console);
481        assert!(output.metrics);
482        assert!(!output.network);
483        assert!(!output.memory);
484    }
485
486    #[test]
487    fn test_debug_output_with_memory() {
488        let output = DebugOutput::none().with_memory();
489        assert!(!output.console);
490        assert!(!output.metrics);
491        assert!(!output.network);
492        assert!(output.memory);
493    }
494
495    #[test]
496    fn test_debug_output_chaining_all() {
497        let output = DebugOutput::none()
498            .with_console()
499            .with_metrics()
500            .with_network()
501            .with_memory();
502
503        assert!(output.console);
504        assert!(output.metrics);
505        assert!(output.network);
506        assert!(output.memory);
507    }
508
509    #[test]
510    fn test_console_level_debug_prefix() {
511        let prefix = ConsoleLevel::Debug.prefix();
512        assert!(prefix.contains("[DEBUG]"));
513    }
514
515    #[test]
516    fn test_console_level_log_prefix() {
517        let prefix = ConsoleLevel::Log.prefix();
518        assert!(prefix.contains("[LOG]"));
519    }
520
521    #[test]
522    fn test_console_level_all_prefixes() {
523        // Test all variants have appropriate prefixes
524        assert!(ConsoleLevel::Debug.prefix().contains("[DEBUG]"));
525        assert!(ConsoleLevel::Info.prefix().contains("[INFO]"));
526        assert!(ConsoleLevel::Log.prefix().contains("[LOG]"));
527        assert!(ConsoleLevel::Warn.prefix().contains("[WARN]"));
528        assert!(ConsoleLevel::Error.prefix().contains("[ERROR]"));
529    }
530
531    #[test]
532    fn test_wasm_runner_debug_output_accessor() {
533        let runner = WasmRunner::new(WasmRunnerConfig::default());
534        let debug_output = runner.debug_output();
535
536        // Default debug output has console and metrics enabled
537        assert!(debug_output.console);
538        assert!(debug_output.metrics);
539        assert!(!debug_output.network);
540        assert!(!debug_output.memory);
541    }
542
543    #[test]
544    fn test_wasm_runner_set_debug_output() {
545        let mut runner = WasmRunner::new(WasmRunnerConfig::default());
546
547        // Set to all enabled
548        runner.set_debug_output(DebugOutput::all());
549        assert!(runner.debug_output().console);
550        assert!(runner.debug_output().metrics);
551        assert!(runner.debug_output().network);
552        assert!(runner.debug_output().memory);
553
554        // Set to none
555        runner.set_debug_output(DebugOutput::none());
556        assert!(!runner.debug_output().console);
557        assert!(!runner.debug_output().metrics);
558        assert!(!runner.debug_output().network);
559        assert!(!runner.debug_output().memory);
560    }
561
562    #[test]
563    fn test_wasm_runner_builder_preserve_state() {
564        let runner = WasmRunnerBuilder::new().preserve_state(false).build();
565        assert!(!runner.config().preserve_state);
566
567        let runner2 = WasmRunnerBuilder::new().preserve_state(true).build();
568        assert!(runner2.config().preserve_state);
569    }
570
571    #[test]
572    fn test_wasm_runner_builder_source_maps() {
573        let runner = WasmRunnerBuilder::new().source_maps(false).build();
574        assert!(!runner.config().source_maps);
575
576        let runner2 = WasmRunnerBuilder::new().source_maps(true).build();
577        assert!(runner2.config().source_maps);
578    }
579
580    #[test]
581    fn test_wasm_runner_builder_debug_output() {
582        let runner = WasmRunnerBuilder::new()
583            .debug_output(DebugOutput::all())
584            .build();
585
586        assert!(runner.debug_output().console);
587        assert!(runner.debug_output().metrics);
588        assert!(runner.debug_output().network);
589        assert!(runner.debug_output().memory);
590    }
591
592    #[test]
593    fn test_hot_reload_event_rebuild_started() {
594        let event = HotReloadEvent::RebuildStarted;
595        assert!(matches!(event, HotReloadEvent::RebuildStarted));
596    }
597
598    #[test]
599    fn test_hot_reload_event_rebuild_failed() {
600        let event = HotReloadEvent::RebuildFailed {
601            errors: vec![
602                "error[E0599]: no method named `foo`".to_string(),
603                "error: aborting due to previous error".to_string(),
604            ],
605        };
606        if let HotReloadEvent::RebuildFailed { errors } = event {
607            assert_eq!(errors.len(), 2);
608            assert!(errors[0].contains("E0599"));
609        } else {
610            panic!("Expected HotReloadEvent::RebuildFailed");
611        }
612    }
613
614    #[test]
615    fn test_hot_reload_event_client_connected() {
616        let event = HotReloadEvent::ClientConnected { id: 42 };
617        if let HotReloadEvent::ClientConnected { id } = event {
618            assert_eq!(id, 42);
619        } else {
620            panic!("Expected HotReloadEvent::ClientConnected");
621        }
622    }
623
624    #[test]
625    fn test_hot_reload_event_client_disconnected() {
626        let event = HotReloadEvent::ClientDisconnected { id: 99 };
627        if let HotReloadEvent::ClientDisconnected { id } = event {
628            assert_eq!(id, 99);
629        } else {
630            panic!("Expected HotReloadEvent::ClientDisconnected");
631        }
632    }
633
634    #[test]
635    fn test_wasm_runner_builder_new_vs_default() {
636        let builder1 = WasmRunnerBuilder::new();
637        let builder2 = WasmRunnerBuilder::default();
638
639        let runner1 = builder1.build();
640        let runner2 = builder2.build();
641
642        assert_eq!(runner1.config().http_port, runner2.config().http_port);
643        assert_eq!(runner1.config().ws_port, runner2.config().ws_port);
644    }
645
646    #[test]
647    fn test_wasm_runner_config_accessor() {
648        let runner = WasmRunnerBuilder::new()
649            .http_port(5000)
650            .ws_port(5001)
651            .hot_reload(true)
652            .build();
653
654        let config = runner.config();
655        assert_eq!(config.http_port, 5000);
656        assert_eq!(config.ws_port, 5001);
657        assert!(config.hot_reload);
658    }
659
660    #[test]
661    fn test_wasm_runner_simulate_multiple_clients() {
662        let mut runner = WasmRunner::new(WasmRunnerConfig::default());
663        runner.simulate_start();
664
665        // Connect multiple clients
666        let id1 = runner.simulate_client_connect();
667        let id2 = runner.simulate_client_connect();
668        let id3 = runner.simulate_client_connect();
669
670        assert_eq!(runner.client_count(), 3);
671
672        // IDs should be sequential and unique
673        assert_eq!(id1, 1);
674        assert_eq!(id2, 2);
675        assert_eq!(id3, 3);
676
677        // Disconnect middle client
678        runner.simulate_client_disconnect(id2);
679        assert_eq!(runner.client_count(), 2);
680
681        // Disconnect non-existent client (should be no-op)
682        runner.simulate_client_disconnect(999);
683        assert_eq!(runner.client_count(), 2);
684    }
685
686    #[test]
687    fn test_console_message_all_levels() {
688        let runner = WasmRunner::new(WasmRunnerConfig::default());
689
690        for level in [
691            ConsoleLevel::Debug,
692            ConsoleLevel::Info,
693            ConsoleLevel::Log,
694            ConsoleLevel::Warn,
695            ConsoleLevel::Error,
696        ] {
697            let msg = ConsoleMessage {
698                level,
699                text: format!("Message at {:?} level", level),
700                timestamp: std::time::SystemTime::now(),
701            };
702            let formatted = runner.format_console_message(&msg);
703            assert!(formatted.contains(&msg.text));
704        }
705    }
706
707    #[test]
708    fn test_hot_reload_event_serialization() {
709        let event = HotReloadEvent::Rebuild {
710            duration: Duration::from_millis(1500),
711            preserved: vec!["GameState".to_string(), "PlayerData".to_string()],
712        };
713
714        let json = serde_json::to_string(&event).unwrap();
715        assert!(json.contains("GameState"));
716        assert!(json.contains("PlayerData"));
717    }
718
719    #[test]
720    fn test_console_message_serialization() {
721        let msg = ConsoleMessage {
722            level: ConsoleLevel::Warn,
723            text: "Warning message".to_string(),
724            timestamp: std::time::SystemTime::now(),
725        };
726
727        let json = serde_json::to_string(&msg).unwrap();
728        let deserialized: ConsoleMessage = serde_json::from_str(&json).unwrap();
729
730        assert_eq!(deserialized.level, ConsoleLevel::Warn);
731        assert_eq!(deserialized.text, "Warning message");
732    }
733
734    #[test]
735    fn test_debug_output_serialization() {
736        let output = DebugOutput::all();
737        let json = serde_json::to_string(&output).unwrap();
738        let deserialized: DebugOutput = serde_json::from_str(&json).unwrap();
739
740        assert!(deserialized.console);
741        assert!(deserialized.metrics);
742        assert!(deserialized.network);
743        assert!(deserialized.memory);
744    }
745
746    #[test]
747    fn test_wasm_runner_builder_all_options() {
748        let runner = WasmRunnerBuilder::new()
749            .http_port(7000)
750            .ws_port(7001)
751            .hot_reload(false)
752            .preserve_state(false)
753            .source_maps(false)
754            .debug_output(DebugOutput::none())
755            .build();
756
757        assert_eq!(runner.config().http_port, 7000);
758        assert_eq!(runner.config().ws_port, 7001);
759        assert!(!runner.config().hot_reload);
760        assert!(!runner.config().preserve_state);
761        assert!(!runner.config().source_maps);
762        assert!(!runner.debug_output().console);
763    }
764
765    #[test]
766    fn test_console_level_equality() {
767        assert_eq!(ConsoleLevel::Debug, ConsoleLevel::Debug);
768        assert_eq!(ConsoleLevel::Info, ConsoleLevel::Info);
769        assert_eq!(ConsoleLevel::Log, ConsoleLevel::Log);
770        assert_eq!(ConsoleLevel::Warn, ConsoleLevel::Warn);
771        assert_eq!(ConsoleLevel::Error, ConsoleLevel::Error);
772
773        assert_ne!(ConsoleLevel::Debug, ConsoleLevel::Error);
774        assert_ne!(ConsoleLevel::Info, ConsoleLevel::Warn);
775    }
776
777    #[test]
778    fn test_wasm_runner_initial_state() {
779        let runner = WasmRunner::new(WasmRunnerConfig::default());
780
781        assert!(!runner.is_running());
782        assert_eq!(runner.client_count(), 0);
783    }
784
785    #[test]
786    fn test_wasm_runner_url_formats() {
787        let runner = WasmRunnerBuilder::new()
788            .http_port(3000)
789            .ws_port(3001)
790            .build();
791
792        assert!(runner.http_url().starts_with("http://"));
793        assert!(runner.http_url().contains("localhost"));
794        assert!(runner.http_url().contains("3000"));
795
796        assert!(runner.ws_url().starts_with("ws://"));
797        assert!(runner.ws_url().contains("localhost"));
798        assert!(runner.ws_url().contains("3001"));
799    }
800
801    #[test]
802    fn test_hot_reload_event_file_changed() {
803        let event = HotReloadEvent::FileChanged {
804            path: "src/game.rs".to_string(),
805        };
806        if let HotReloadEvent::FileChanged { path } = event {
807            assert_eq!(path, "src/game.rs");
808        } else {
809            panic!("Expected HotReloadEvent::FileChanged");
810        }
811    }
812
813    #[test]
814    fn test_hot_reload_event_rebuild_details() {
815        let event = HotReloadEvent::Rebuild {
816            duration: Duration::from_secs(2),
817            preserved: vec!["AppState".to_string()],
818        };
819        if let HotReloadEvent::Rebuild {
820            duration,
821            preserved,
822        } = event
823        {
824            assert_eq!(duration, Duration::from_secs(2));
825            assert_eq!(preserved.len(), 1);
826            assert_eq!(preserved[0], "AppState");
827        } else {
828            panic!("Expected HotReloadEvent::Rebuild");
829        }
830    }
831
832    #[test]
833    fn test_wasm_runner_stop_clears_clients() {
834        let mut runner = WasmRunner::new(WasmRunnerConfig::default());
835        runner.simulate_start();
836
837        runner.simulate_client_connect();
838        runner.simulate_client_connect();
839        assert_eq!(runner.client_count(), 2);
840
841        runner.simulate_stop();
842        assert!(!runner.is_running());
843        assert_eq!(runner.client_count(), 0);
844    }
845
846    #[test]
847    fn test_wasm_runner_builder_default_debug_output() {
848        let runner = WasmRunnerBuilder::default().build();
849
850        // Default is DebugOutput::default() which has console and metrics enabled
851        assert!(runner.debug_output().console);
852        assert!(runner.debug_output().metrics);
853        assert!(!runner.debug_output().network);
854        assert!(!runner.debug_output().memory);
855    }
856
857    #[test]
858    fn test_console_level_serialization_all_variants() {
859        for level in [
860            ConsoleLevel::Debug,
861            ConsoleLevel::Info,
862            ConsoleLevel::Log,
863            ConsoleLevel::Warn,
864            ConsoleLevel::Error,
865        ] {
866            let json = serde_json::to_string(&level).unwrap();
867            let deserialized: ConsoleLevel = serde_json::from_str(&json).unwrap();
868            assert_eq!(level, deserialized);
869        }
870    }
871
872    #[test]
873    fn test_hot_reload_event_all_variants_serialization() {
874        // FileChanged
875        let event1 = HotReloadEvent::FileChanged {
876            path: "test.rs".to_string(),
877        };
878        let json1 = serde_json::to_string(&event1).unwrap();
879        assert!(json1.contains("test.rs"));
880
881        // RebuildStarted
882        let event2 = HotReloadEvent::RebuildStarted;
883        let json2 = serde_json::to_string(&event2).unwrap();
884        assert!(json2.contains("RebuildStarted"));
885
886        // RebuildFailed
887        let event3 = HotReloadEvent::RebuildFailed {
888            errors: vec!["error".to_string()],
889        };
890        let json3 = serde_json::to_string(&event3).unwrap();
891        assert!(json3.contains("error"));
892
893        // ClientConnected
894        let event4 = HotReloadEvent::ClientConnected { id: 1 };
895        let json4 = serde_json::to_string(&event4).unwrap();
896        assert!(json4.contains('1'));
897
898        // ClientDisconnected
899        let event5 = HotReloadEvent::ClientDisconnected { id: 2 };
900        let json5 = serde_json::to_string(&event5).unwrap();
901        assert!(json5.contains('2'));
902    }
903}