Skip to main content

jugar_probar/brick/
worker.rs

1//! WorkerBrick: Web Worker code generation from brick definitions (PROBAR-SPEC-009-P7)
2//!
3//! Generates both JavaScript Worker code and Rust web_sys bindings
4//! from a single brick definition. Zero hand-written JavaScript.
5//!
6//! # Example
7//!
8//! ```rust,ignore
9//! use probar::brick::worker::{WorkerBrick, BrickWorkerMessage, BrickWorkerMessageDirection};
10//!
11//! let worker = WorkerBrick::new("transcription")
12//!     .message(BrickWorkerMessage::new("init", BrickWorkerMessageDirection::ToWorker)
13//!         .field("modelUrl", FieldType::String)
14//!         .field("buffer", FieldType::SharedArrayBuffer))
15//!     .message(BrickWorkerMessage::new("ready", BrickWorkerMessageDirection::FromWorker))
16//!     .transition("uninitialized", "init", "loading")
17//!     .transition("loading", "ready", "ready");
18//!
19//! // Generate JavaScript
20//! let js = worker.to_worker_js();
21//!
22//! // Generate Rust bindings
23//! let rust = worker.to_rust_bindings();
24//! ```
25
26use super::{Brick, BrickAssertion, BrickBudget, BrickVerification};
27use std::time::Duration;
28
29/// Direction of worker message
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum BrickWorkerMessageDirection {
32    /// Message sent to worker (main → worker)
33    ToWorker,
34    /// Message sent from worker (worker → main)
35    FromWorker,
36    /// Message can be sent in either direction
37    Bidirectional,
38}
39
40/// Field type for worker messages
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum FieldType {
43    /// JavaScript string
44    String,
45    /// JavaScript number (f64)
46    Number,
47    /// JavaScript boolean
48    Boolean,
49    /// SharedArrayBuffer for audio/data transfer
50    SharedArrayBuffer,
51    /// Float32Array for audio samples
52    Float32Array,
53    /// Nested object with fields
54    Object(Vec<MessageField>),
55    /// Optional field
56    Optional(Box<FieldType>),
57}
58
59impl FieldType {
60    /// Get TypeScript type annotation
61    #[must_use]
62    pub fn to_typescript(&self) -> String {
63        match self {
64            Self::String => "string".into(),
65            Self::Number => "number".into(),
66            Self::Boolean => "boolean".into(),
67            Self::SharedArrayBuffer => "SharedArrayBuffer".into(),
68            Self::Float32Array => "Float32Array".into(),
69            Self::Object(fields) => {
70                let field_types: Vec<_> = fields
71                    .iter()
72                    .map(|f| format!("{}: {}", f.name, f.field_type.to_typescript()))
73                    .collect();
74                format!("{{ {} }}", field_types.join(", "))
75            }
76            Self::Optional(inner) => format!("{} | undefined", inner.to_typescript()),
77        }
78    }
79
80    /// Get Rust type annotation
81    #[must_use]
82    pub fn to_rust(&self) -> String {
83        match self {
84            Self::String => "String".into(),
85            Self::Number => "f64".into(),
86            Self::Boolean => "bool".into(),
87            Self::SharedArrayBuffer => "js_sys::SharedArrayBuffer".into(),
88            Self::Float32Array => "js_sys::Float32Array".into(),
89            Self::Object(_) => "serde_json::Value".into(),
90            Self::Optional(inner) => format!("Option<{}>", inner.to_rust()),
91        }
92    }
93}
94
95/// A field in a worker message
96#[derive(Debug, Clone, PartialEq, Eq)]
97pub struct MessageField {
98    /// Field name
99    pub name: String,
100    /// Field type
101    pub field_type: FieldType,
102    /// Whether the field is required
103    pub required: bool,
104}
105
106impl MessageField {
107    /// Create a new required field
108    #[must_use]
109    pub fn new(name: impl Into<String>, field_type: FieldType) -> Self {
110        Self {
111            name: name.into(),
112            field_type,
113            required: true,
114        }
115    }
116
117    /// Create an optional field
118    #[must_use]
119    pub fn optional(name: impl Into<String>, field_type: FieldType) -> Self {
120        Self {
121            name: name.into(),
122            field_type: FieldType::Optional(Box::new(field_type)),
123            required: false,
124        }
125    }
126}
127
128/// A worker message definition
129#[derive(Debug, Clone)]
130pub struct BrickWorkerMessage {
131    /// Message type name (PascalCase for Rust, lowercase for JS)
132    pub name: String,
133    /// Direction of the message
134    pub direction: BrickWorkerMessageDirection,
135    /// Message fields
136    pub fields: Vec<MessageField>,
137    /// Include trace context for distributed tracing
138    pub trace_context: bool,
139}
140
141impl BrickWorkerMessage {
142    /// Create a new worker message
143    #[must_use]
144    pub fn new(name: impl Into<String>, direction: BrickWorkerMessageDirection) -> Self {
145        Self {
146            name: name.into(),
147            direction,
148            fields: Vec::new(),
149            trace_context: true, // Default to including trace context
150        }
151    }
152
153    /// Add a field to the message
154    #[must_use]
155    pub fn field(mut self, name: impl Into<String>, field_type: FieldType) -> Self {
156        self.fields.push(MessageField::new(name, field_type));
157        self
158    }
159
160    /// Add an optional field
161    #[must_use]
162    pub fn optional_field(mut self, name: impl Into<String>, field_type: FieldType) -> Self {
163        self.fields.push(MessageField::optional(name, field_type));
164        self
165    }
166
167    /// Disable trace context for this message
168    #[must_use]
169    pub fn without_trace(mut self) -> Self {
170        self.trace_context = false;
171        self
172    }
173
174    /// Get the JavaScript type name (lowercase)
175    #[must_use]
176    pub fn js_type_name(&self) -> String {
177        self.name.to_lowercase()
178    }
179
180    /// Get the Rust type name (PascalCase)
181    #[must_use]
182    pub fn rust_type_name(&self) -> String {
183        // Convert to PascalCase
184        let mut result = String::new();
185        let mut capitalize_next = true;
186        for c in self.name.chars() {
187            if c == '_' || c == '-' {
188                capitalize_next = true;
189            } else if capitalize_next {
190                result.push(c.to_ascii_uppercase());
191                capitalize_next = false;
192            } else {
193                result.push(c);
194            }
195        }
196        result
197    }
198}
199
200/// A state transition in the worker state machine
201#[derive(Debug, Clone)]
202pub struct WorkerTransition {
203    /// Source state
204    pub from: String,
205    /// Message that triggers the transition
206    pub message: String,
207    /// Target state
208    pub to: String,
209    /// Optional action to execute
210    pub action: Option<String>,
211}
212
213impl WorkerTransition {
214    /// Create a new transition
215    #[must_use]
216    pub fn new(from: impl Into<String>, message: impl Into<String>, to: impl Into<String>) -> Self {
217        Self {
218            from: from.into(),
219            message: message.into(),
220            to: to.into(),
221            action: None,
222        }
223    }
224
225    /// Add an action to the transition
226    #[must_use]
227    pub fn with_action(mut self, action: impl Into<String>) -> Self {
228        self.action = Some(action.into());
229        self
230    }
231}
232
233/// WorkerBrick: Generates Web Worker code from brick definition
234#[derive(Debug, Clone)]
235pub struct WorkerBrick {
236    /// Worker name
237    name: String,
238    /// Message definitions
239    messages: Vec<BrickWorkerMessage>,
240    /// State machine transitions
241    transitions: Vec<WorkerTransition>,
242    /// Initial state
243    initial_state: String,
244    /// All states
245    states: Vec<String>,
246}
247
248impl WorkerBrick {
249    /// Create a new worker brick
250    #[must_use]
251    pub fn new(name: impl Into<String>) -> Self {
252        Self {
253            name: name.into(),
254            messages: Vec::new(),
255            transitions: Vec::new(),
256            initial_state: "uninitialized".into(),
257            states: vec!["uninitialized".into()],
258        }
259    }
260
261    /// Add a message definition
262    #[must_use]
263    pub fn message(mut self, msg: BrickWorkerMessage) -> Self {
264        self.messages.push(msg);
265        self
266    }
267
268    /// Add a state
269    #[must_use]
270    pub fn state(mut self, state: impl Into<String>) -> Self {
271        let state = state.into();
272        if !self.states.contains(&state) {
273            self.states.push(state);
274        }
275        self
276    }
277
278    /// Set the initial state
279    #[must_use]
280    pub fn initial(mut self, state: impl Into<String>) -> Self {
281        self.initial_state = state.into();
282        self
283    }
284
285    /// Add a state transition
286    #[must_use]
287    pub fn transition(
288        mut self,
289        from: impl Into<String>,
290        message: impl Into<String>,
291        to: impl Into<String>,
292    ) -> Self {
293        let from = from.into();
294        let to = to.into();
295
296        // Auto-add states
297        if !self.states.contains(&from) {
298            self.states.push(from.clone());
299        }
300        if !self.states.contains(&to) {
301            self.states.push(to.clone());
302        }
303
304        self.transitions
305            .push(WorkerTransition::new(from, message, to));
306        self
307    }
308
309    /// Add a transition with action
310    #[must_use]
311    pub fn transition_with_action(
312        mut self,
313        from: impl Into<String>,
314        message: impl Into<String>,
315        to: impl Into<String>,
316        action: impl Into<String>,
317    ) -> Self {
318        let from = from.into();
319        let to = to.into();
320
321        if !self.states.contains(&from) {
322            self.states.push(from.clone());
323        }
324        if !self.states.contains(&to) {
325            self.states.push(to.clone());
326        }
327
328        self.transitions
329            .push(WorkerTransition::new(from, message, to).with_action(action));
330        self
331    }
332
333    /// Get messages sent to worker
334    #[must_use]
335    pub fn to_worker_messages(&self) -> Vec<&BrickWorkerMessage> {
336        self.messages
337            .iter()
338            .filter(|m| {
339                matches!(
340                    m.direction,
341                    BrickWorkerMessageDirection::ToWorker
342                        | BrickWorkerMessageDirection::Bidirectional
343                )
344            })
345            .collect()
346    }
347
348    /// Get messages sent from worker
349    #[must_use]
350    pub fn from_worker_messages(&self) -> Vec<&BrickWorkerMessage> {
351        self.messages
352            .iter()
353            .filter(|m| {
354                matches!(
355                    m.direction,
356                    BrickWorkerMessageDirection::FromWorker
357                        | BrickWorkerMessageDirection::Bidirectional
358                )
359            })
360            .collect()
361    }
362
363    /// Generate JavaScript Worker code
364    #[must_use]
365    pub fn to_worker_js(&self) -> String {
366        let mut js = String::new();
367
368        // Header
369        js.push_str(&format!(
370            "// {} Worker (ES Module)\n",
371            to_pascal_case(&self.name)
372        ));
373        js.push_str("// Generated by probar - DO NOT EDIT MANUALLY\n\n");
374
375        // State variable
376        js.push_str(&format!("let workerState = '{}';\n\n", self.initial_state));
377
378        // Message handler
379        js.push_str("self.onmessage = async (e) => {\n");
380        js.push_str("    const msg = e.data;\n");
381        js.push_str("    const _trace = msg._trace; // Dapper trace context\n\n");
382        js.push_str("    switch (msg.type) {\n");
383
384        // Generate case for each to-worker message
385        for msg in self.to_worker_messages() {
386            let js_type = msg.js_type_name();
387
388            js.push_str(&format!("        case '{}':\n", js_type));
389
390            // Find transitions triggered by this message
391            let transitions: Vec<_> = self
392                .transitions
393                .iter()
394                .filter(|t| t.message.to_lowercase() == js_type)
395                .collect();
396
397            if transitions.is_empty() {
398                js.push_str(&format!(
399                    "            console.log('[Worker] Received {} (no state change)');\n",
400                    js_type
401                ));
402            } else {
403                // Generate state machine validation
404                let valid_from_states: Vec<_> = transitions
405                    .iter()
406                    .map(|t| format!("'{}'", t.from))
407                    .collect();
408
409                js.push_str(&format!(
410                    "            if (![{}].includes(workerState)) {{\n",
411                    valid_from_states.join(", ")
412                ));
413                js.push_str(&format!(
414                    "                console.warn('[Worker] Invalid state for {}: ' + workerState);\n",
415                    js_type
416                ));
417                js.push_str("                return;\n");
418                js.push_str("            }\n");
419
420                // State transition
421                if let Some(t) = transitions.first() {
422                    js.push_str(&format!("            workerState = '{}';\n", t.to));
423                    if let Some(ref action) = t.action {
424                        js.push_str(&format!("            {};\n", action));
425                    }
426                }
427            }
428
429            js.push_str("            break;\n\n");
430        }
431
432        // Default case (Yuan Gate - no swallowing)
433        js.push_str("        default:\n");
434        js.push_str("            throw new Error('[Worker] Unknown message type: ' + msg.type);\n");
435        js.push_str("    }\n");
436        js.push_str("};\n\n");
437
438        // Helper to post message back
439        js.push_str("function postResult(type, data, trace) {\n");
440        js.push_str("    self.postMessage({ type, ...data, _trace: trace });\n");
441        js.push_str("}\n\n");
442
443        // Log module loaded
444        js.push_str(&format!(
445            "console.log('[Worker] {} module loaded');\n",
446            to_pascal_case(&self.name)
447        ));
448
449        js
450    }
451
452    /// Generate Rust web_sys bindings
453    #[must_use]
454    pub fn to_rust_bindings(&self) -> String {
455        let mut rust = String::new();
456
457        // Header
458        rust.push_str(&format!(
459            "//! {} Worker Bindings\n",
460            to_pascal_case(&self.name)
461        ));
462        rust.push_str("//! Generated by probar - DO NOT EDIT MANUALLY\n\n");
463        rust.push_str("use serde::{Deserialize, Serialize};\n\n");
464
465        // ToWorker enum
466        rust.push_str("#[derive(Debug, Clone, Serialize, Deserialize)]\n");
467        rust.push_str("#[serde(tag = \"type\", rename_all = \"lowercase\")]\n");
468        rust.push_str("pub enum ToWorker {\n");
469
470        for msg in self.to_worker_messages() {
471            let name = msg.rust_type_name();
472            if msg.fields.is_empty() {
473                rust.push_str(&format!("    {},\n", name));
474            } else {
475                rust.push_str(&format!("    {} {{\n", name));
476                for field in &msg.fields {
477                    let rust_type = field.field_type.to_rust();
478                    rust.push_str(&format!(
479                        "        {}: {},\n",
480                        to_snake_case(&field.name),
481                        rust_type
482                    ));
483                }
484                rust.push_str("    },\n");
485            }
486        }
487        rust.push_str("}\n\n");
488
489        // FromWorker enum
490        rust.push_str("#[derive(Debug, Clone, Serialize, Deserialize)]\n");
491        rust.push_str("#[serde(tag = \"type\", rename_all = \"lowercase\")]\n");
492        rust.push_str("pub enum FromWorker {\n");
493
494        for msg in self.from_worker_messages() {
495            let name = msg.rust_type_name();
496            if msg.fields.is_empty() {
497                rust.push_str(&format!("    {},\n", name));
498            } else {
499                rust.push_str(&format!("    {} {{\n", name));
500                for field in &msg.fields {
501                    let rust_type = field.field_type.to_rust();
502                    rust.push_str(&format!(
503                        "        {}: {},\n",
504                        to_snake_case(&field.name),
505                        rust_type
506                    ));
507                }
508                rust.push_str("    },\n");
509            }
510        }
511        rust.push_str("}\n\n");
512
513        // State enum
514        rust.push_str("#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n");
515        rust.push_str("pub enum WorkerState {\n");
516        for state in &self.states {
517            rust.push_str(&format!("    {},\n", to_pascal_case(state)));
518        }
519        rust.push_str("}\n\n");
520
521        rust.push_str(&format!(
522            "impl Default for WorkerState {{\n    fn default() -> Self {{\n        Self::{}\n    }}\n}}\n",
523            to_pascal_case(&self.initial_state)
524        ));
525
526        rust
527    }
528
529    /// Generate TypeScript type definitions
530    #[must_use]
531    pub fn to_typescript_defs(&self) -> String {
532        let mut ts = String::new();
533
534        ts.push_str(&format!("// {} Worker Types\n", to_pascal_case(&self.name)));
535        ts.push_str("// Generated by probar - DO NOT EDIT MANUALLY\n\n");
536
537        // Trace context type
538        ts.push_str("interface TraceContext {\n");
539        ts.push_str("    trace_id: string;\n");
540        ts.push_str("    parent_span_id: string;\n");
541        ts.push_str("    span_id: string;\n");
542        ts.push_str("}\n\n");
543
544        // Message types
545        for msg in &self.messages {
546            ts.push_str(&format!("interface {}Message {{\n", msg.rust_type_name()));
547            ts.push_str(&format!("    type: '{}';\n", msg.js_type_name()));
548            for field in &msg.fields {
549                let ts_type = field.field_type.to_typescript();
550                if field.required {
551                    ts.push_str(&format!("    {}: {};\n", field.name, ts_type));
552                } else {
553                    ts.push_str(&format!("    {}?: {};\n", field.name, ts_type));
554                }
555            }
556            ts.push_str("    _trace?: TraceContext;\n");
557            ts.push_str("}\n\n");
558        }
559
560        ts
561    }
562}
563
564impl Brick for WorkerBrick {
565    fn brick_name(&self) -> &'static str {
566        "WorkerBrick"
567    }
568
569    fn assertions(&self) -> &[BrickAssertion] {
570        // WorkerBrick assertions are verified by JS validator
571        &[]
572    }
573
574    fn budget(&self) -> BrickBudget {
575        // Worker code generation is not render-bound
576        BrickBudget::uniform(1000)
577    }
578
579    fn verify(&self) -> BrickVerification {
580        let mut passed = Vec::new();
581        let mut failed = Vec::new();
582
583        // Verify state machine completeness
584        for transition in &self.transitions {
585            if !self.states.contains(&transition.from) {
586                failed.push((
587                    BrickAssertion::Custom {
588                        name: "state_exists".into(),
589                        validator_id: 1,
590                    },
591                    format!("State '{}' not defined", transition.from),
592                ));
593            }
594            if !self.states.contains(&transition.to) {
595                failed.push((
596                    BrickAssertion::Custom {
597                        name: "state_exists".into(),
598                        validator_id: 1,
599                    },
600                    format!("State '{}' not defined", transition.to),
601                ));
602            }
603        }
604
605        // Verify messages have corresponding transitions
606        for msg in self.to_worker_messages() {
607            let has_transition = self
608                .transitions
609                .iter()
610                .any(|t| t.message.to_lowercase() == msg.js_type_name());
611
612            if has_transition {
613                passed.push(BrickAssertion::Custom {
614                    name: format!("message_{}_handled", msg.name),
615                    validator_id: 2,
616                });
617            } else {
618                failed.push((
619                    BrickAssertion::Custom {
620                        name: format!("message_{}_handled", msg.name),
621                        validator_id: 2,
622                    },
623                    format!("Message '{}' has no state transition", msg.name),
624                ));
625            }
626        }
627
628        if failed.is_empty() {
629            passed.push(BrickAssertion::Custom {
630                name: "state_machine_valid".into(),
631                validator_id: 3,
632            });
633        }
634
635        BrickVerification {
636            passed,
637            failed,
638            verification_time: Duration::from_micros(100),
639        }
640    }
641
642    fn to_html(&self) -> String {
643        // WorkerBrick doesn't generate HTML
644        String::new()
645    }
646
647    fn to_css(&self) -> String {
648        // WorkerBrick doesn't generate CSS
649        String::new()
650    }
651
652    fn test_id(&self) -> Option<&str> {
653        None
654    }
655}
656
657/// Convert string to PascalCase
658fn to_pascal_case(s: &str) -> String {
659    let mut result = String::new();
660    let mut capitalize_next = true;
661
662    for c in s.chars() {
663        if c == '_' || c == '-' || c == ' ' {
664            capitalize_next = true;
665        } else if capitalize_next {
666            result.push(c.to_ascii_uppercase());
667            capitalize_next = false;
668        } else {
669            result.push(c);
670        }
671    }
672
673    result
674}
675
676/// Convert string to snake_case
677fn to_snake_case(s: &str) -> String {
678    let mut result = String::new();
679
680    for (i, c) in s.chars().enumerate() {
681        if c.is_ascii_uppercase() {
682            if i > 0 {
683                result.push('_');
684            }
685            result.push(c.to_ascii_lowercase());
686        } else if c == '-' {
687            result.push('_');
688        } else {
689            result.push(c);
690        }
691    }
692
693    result
694}
695
696#[cfg(test)]
697#[allow(clippy::unwrap_used, clippy::expect_used)]
698mod tests {
699    use super::*;
700
701    #[test]
702    fn test_worker_brick_basic() {
703        let worker = WorkerBrick::new("transcription")
704            .message(BrickWorkerMessage::new(
705                "init",
706                BrickWorkerMessageDirection::ToWorker,
707            ))
708            .message(BrickWorkerMessage::new(
709                "ready",
710                BrickWorkerMessageDirection::FromWorker,
711            ))
712            .transition("uninitialized", "init", "ready");
713
714        assert_eq!(worker.name, "transcription");
715        assert_eq!(worker.messages.len(), 2);
716        assert_eq!(worker.transitions.len(), 1);
717    }
718
719    #[test]
720    fn test_worker_brick_js_generation() {
721        let worker = WorkerBrick::new("test")
722            .message(BrickWorkerMessage::new(
723                "ping",
724                BrickWorkerMessageDirection::ToWorker,
725            ))
726            .message(BrickWorkerMessage::new(
727                "pong",
728                BrickWorkerMessageDirection::FromWorker,
729            ))
730            .transition("uninitialized", "ping", "ready");
731
732        let js = worker.to_worker_js();
733
734        assert!(js.contains("self.onmessage"));
735        assert!(js.contains("case 'ping':"));
736        assert!(js.contains("workerState = 'ready'"));
737        assert!(js.contains("Generated by probar"));
738    }
739
740    #[test]
741    fn test_worker_brick_rust_bindings() {
742        let worker = WorkerBrick::new("test")
743            .message(
744                BrickWorkerMessage::new("init", BrickWorkerMessageDirection::ToWorker)
745                    .field("url", FieldType::String),
746            )
747            .message(BrickWorkerMessage::new(
748                "ready",
749                BrickWorkerMessageDirection::FromWorker,
750            ))
751            .transition("uninitialized", "init", "ready");
752
753        let rust = worker.to_rust_bindings();
754
755        assert!(rust.contains("pub enum ToWorker"));
756        assert!(rust.contains("pub enum FromWorker"));
757        assert!(rust.contains("pub enum WorkerState"));
758        assert!(rust.contains("url: String"));
759    }
760
761    #[test]
762    fn test_worker_brick_verification() {
763        let worker = WorkerBrick::new("test")
764            .message(BrickWorkerMessage::new(
765                "init",
766                BrickWorkerMessageDirection::ToWorker,
767            ))
768            .transition("uninitialized", "init", "ready");
769
770        let result = worker.verify();
771        assert!(result.is_valid());
772    }
773
774    #[test]
775    fn test_field_type_typescript() {
776        assert_eq!(FieldType::String.to_typescript(), "string");
777        assert_eq!(FieldType::Number.to_typescript(), "number");
778        assert_eq!(FieldType::Boolean.to_typescript(), "boolean");
779        assert_eq!(
780            FieldType::SharedArrayBuffer.to_typescript(),
781            "SharedArrayBuffer"
782        );
783    }
784
785    #[test]
786    fn test_field_type_rust() {
787        assert_eq!(FieldType::String.to_rust(), "String");
788        assert_eq!(FieldType::Number.to_rust(), "f64");
789        assert_eq!(FieldType::Boolean.to_rust(), "bool");
790    }
791
792    #[test]
793    fn test_to_pascal_case() {
794        assert_eq!(to_pascal_case("hello_world"), "HelloWorld");
795        assert_eq!(to_pascal_case("hello-world"), "HelloWorld");
796        assert_eq!(to_pascal_case("helloWorld"), "HelloWorld");
797    }
798
799    #[test]
800    fn test_to_snake_case() {
801        assert_eq!(to_snake_case("helloWorld"), "hello_world");
802        assert_eq!(to_snake_case("HelloWorld"), "hello_world");
803        assert_eq!(to_snake_case("model-url"), "model_url");
804    }
805
806    // ========================================================================
807    // Additional comprehensive tests for 95%+ coverage
808    // ========================================================================
809
810    #[test]
811    fn test_field_type_float32array() {
812        assert_eq!(FieldType::Float32Array.to_typescript(), "Float32Array");
813        assert_eq!(FieldType::Float32Array.to_rust(), "js_sys::Float32Array");
814    }
815
816    #[test]
817    fn test_field_type_object_typescript() {
818        let fields = vec![
819            MessageField::new("name", FieldType::String),
820            MessageField::new("count", FieldType::Number),
821        ];
822        let object_type = FieldType::Object(fields);
823        let ts = object_type.to_typescript();
824        assert!(ts.contains("name: string"));
825        assert!(ts.contains("count: number"));
826        assert!(ts.starts_with("{ "));
827        assert!(ts.ends_with(" }"));
828    }
829
830    #[test]
831    fn test_field_type_object_rust() {
832        let fields = vec![MessageField::new("data", FieldType::String)];
833        let object_type = FieldType::Object(fields);
834        assert_eq!(object_type.to_rust(), "serde_json::Value");
835    }
836
837    #[test]
838    fn test_field_type_optional_typescript() {
839        let optional = FieldType::Optional(Box::new(FieldType::Number));
840        assert_eq!(optional.to_typescript(), "number | undefined");
841    }
842
843    #[test]
844    fn test_field_type_optional_rust() {
845        let optional = FieldType::Optional(Box::new(FieldType::Boolean));
846        assert_eq!(optional.to_rust(), "Option<bool>");
847    }
848
849    #[test]
850    fn test_message_field_new() {
851        let field = MessageField::new("testField", FieldType::Number);
852        assert_eq!(field.name, "testField");
853        assert_eq!(field.field_type, FieldType::Number);
854        assert!(field.required);
855    }
856
857    #[test]
858    fn test_message_field_optional() {
859        let field = MessageField::optional("optionalField", FieldType::String);
860        assert_eq!(field.name, "optionalField");
861        assert!(!field.required);
862        assert!(matches!(field.field_type, FieldType::Optional(_)));
863    }
864
865    #[test]
866    fn test_brick_worker_message_new() {
867        let msg = BrickWorkerMessage::new("testMsg", BrickWorkerMessageDirection::ToWorker);
868        assert_eq!(msg.name, "testMsg");
869        assert_eq!(msg.direction, BrickWorkerMessageDirection::ToWorker);
870        assert!(msg.fields.is_empty());
871        assert!(msg.trace_context); // Default is true
872    }
873
874    #[test]
875    fn test_brick_worker_message_field() {
876        let msg = BrickWorkerMessage::new("msg", BrickWorkerMessageDirection::ToWorker)
877            .field("url", FieldType::String)
878            .field("count", FieldType::Number);
879        assert_eq!(msg.fields.len(), 2);
880        assert_eq!(msg.fields[0].name, "url");
881        assert_eq!(msg.fields[1].name, "count");
882    }
883
884    #[test]
885    fn test_brick_worker_message_optional_field() {
886        let msg = BrickWorkerMessage::new("msg", BrickWorkerMessageDirection::ToWorker)
887            .optional_field("extra", FieldType::String);
888        assert_eq!(msg.fields.len(), 1);
889        assert!(!msg.fields[0].required);
890    }
891
892    #[test]
893    fn test_brick_worker_message_without_trace() {
894        let msg =
895            BrickWorkerMessage::new("msg", BrickWorkerMessageDirection::ToWorker).without_trace();
896        assert!(!msg.trace_context);
897    }
898
899    #[test]
900    fn test_brick_worker_message_js_type_name() {
901        let msg = BrickWorkerMessage::new("InitModel", BrickWorkerMessageDirection::ToWorker);
902        assert_eq!(msg.js_type_name(), "initmodel");
903    }
904
905    #[test]
906    fn test_brick_worker_message_rust_type_name() {
907        let msg = BrickWorkerMessage::new("init_model", BrickWorkerMessageDirection::ToWorker);
908        assert_eq!(msg.rust_type_name(), "InitModel");
909
910        let msg2 = BrickWorkerMessage::new("load-audio", BrickWorkerMessageDirection::ToWorker);
911        assert_eq!(msg2.rust_type_name(), "LoadAudio");
912    }
913
914    #[test]
915    fn test_worker_transition_new() {
916        let transition = WorkerTransition::new("state1", "event", "state2");
917        assert_eq!(transition.from, "state1");
918        assert_eq!(transition.message, "event");
919        assert_eq!(transition.to, "state2");
920        assert!(transition.action.is_none());
921    }
922
923    #[test]
924    fn test_worker_transition_with_action() {
925        let transition = WorkerTransition::new("s1", "e", "s2").with_action("doSomething()");
926        assert_eq!(transition.action, Some("doSomething()".to_string()));
927    }
928
929    #[test]
930    fn test_worker_brick_state() {
931        let worker = WorkerBrick::new("test")
932            .state("custom_state")
933            .state("another_state");
934
935        assert!(worker.states.contains(&"custom_state".to_string()));
936        assert!(worker.states.contains(&"another_state".to_string()));
937    }
938
939    #[test]
940    fn test_worker_brick_state_dedup() {
941        let worker = WorkerBrick::new("test").state("custom").state("custom"); // Duplicate
942
943        // Should only contain unique states
944        let count = worker.states.iter().filter(|s| *s == "custom").count();
945        assert_eq!(count, 1);
946    }
947
948    #[test]
949    fn test_worker_brick_initial() {
950        let worker = WorkerBrick::new("test").initial("ready");
951        assert_eq!(worker.initial_state, "ready");
952    }
953
954    #[test]
955    fn test_worker_brick_transition_auto_adds_states() {
956        let worker = WorkerBrick::new("test").transition("new_from", "event", "new_to");
957
958        assert!(worker.states.contains(&"new_from".to_string()));
959        assert!(worker.states.contains(&"new_to".to_string()));
960    }
961
962    #[test]
963    fn test_worker_brick_transition_with_action() {
964        let worker = WorkerBrick::new("test").transition_with_action(
965            "s1",
966            "evt",
967            "s2",
968            "console.log('hello')",
969        );
970
971        assert_eq!(worker.transitions.len(), 1);
972        assert_eq!(
973            worker.transitions[0].action,
974            Some("console.log('hello')".to_string())
975        );
976        assert!(worker.states.contains(&"s1".to_string()));
977        assert!(worker.states.contains(&"s2".to_string()));
978    }
979
980    #[test]
981    fn test_worker_brick_to_worker_messages() {
982        let worker = WorkerBrick::new("test")
983            .message(BrickWorkerMessage::new(
984                "to1",
985                BrickWorkerMessageDirection::ToWorker,
986            ))
987            .message(BrickWorkerMessage::new(
988                "from1",
989                BrickWorkerMessageDirection::FromWorker,
990            ))
991            .message(BrickWorkerMessage::new(
992                "bi1",
993                BrickWorkerMessageDirection::Bidirectional,
994            ));
995
996        let to_msgs = worker.to_worker_messages();
997        assert_eq!(to_msgs.len(), 2); // ToWorker + Bidirectional
998    }
999
1000    #[test]
1001    fn test_worker_brick_from_worker_messages() {
1002        let worker = WorkerBrick::new("test")
1003            .message(BrickWorkerMessage::new(
1004                "to1",
1005                BrickWorkerMessageDirection::ToWorker,
1006            ))
1007            .message(BrickWorkerMessage::new(
1008                "from1",
1009                BrickWorkerMessageDirection::FromWorker,
1010            ))
1011            .message(BrickWorkerMessage::new(
1012                "bi1",
1013                BrickWorkerMessageDirection::Bidirectional,
1014            ));
1015
1016        let from_msgs = worker.from_worker_messages();
1017        assert_eq!(from_msgs.len(), 2); // FromWorker + Bidirectional
1018    }
1019
1020    #[test]
1021    fn test_worker_brick_js_with_no_transitions() {
1022        let worker = WorkerBrick::new("test").message(BrickWorkerMessage::new(
1023            "ping",
1024            BrickWorkerMessageDirection::ToWorker,
1025        ));
1026
1027        let js = worker.to_worker_js();
1028        assert!(js.contains("no state change"));
1029    }
1030
1031    #[test]
1032    fn test_worker_brick_js_with_action() {
1033        let worker = WorkerBrick::new("test")
1034            .message(BrickWorkerMessage::new(
1035                "start",
1036                BrickWorkerMessageDirection::ToWorker,
1037            ))
1038            .transition_with_action("uninitialized", "start", "running", "startProcessing()");
1039
1040        let js = worker.to_worker_js();
1041        assert!(js.contains("startProcessing()"));
1042    }
1043
1044    #[test]
1045    fn test_worker_brick_rust_bindings_empty_fields() {
1046        let worker = WorkerBrick::new("test")
1047            .message(BrickWorkerMessage::new(
1048                "ping",
1049                BrickWorkerMessageDirection::ToWorker,
1050            ))
1051            .message(BrickWorkerMessage::new(
1052                "pong",
1053                BrickWorkerMessageDirection::FromWorker,
1054            ))
1055            .transition("uninitialized", "ping", "ready");
1056
1057        let rust = worker.to_rust_bindings();
1058        assert!(rust.contains("Ping,"));
1059        assert!(rust.contains("Pong,"));
1060    }
1061
1062    #[test]
1063    fn test_worker_brick_rust_bindings_with_fields() {
1064        let worker = WorkerBrick::new("test")
1065            .message(
1066                BrickWorkerMessage::new("result", BrickWorkerMessageDirection::FromWorker)
1067                    .field("text", FieldType::String)
1068                    .field("confidence", FieldType::Number),
1069            )
1070            .transition("uninitialized", "init", "ready");
1071
1072        let rust = worker.to_rust_bindings();
1073        assert!(rust.contains("text: String"));
1074        assert!(rust.contains("confidence: f64"));
1075    }
1076
1077    #[test]
1078    fn test_worker_brick_typescript_defs() {
1079        let worker = WorkerBrick::new("test").message(
1080            BrickWorkerMessage::new("config", BrickWorkerMessageDirection::ToWorker)
1081                .field("url", FieldType::String)
1082                .optional_field("timeout", FieldType::Number),
1083        );
1084
1085        let ts = worker.to_typescript_defs();
1086        assert!(ts.contains("interface ConfigMessage"));
1087        assert!(ts.contains("type: 'config'"));
1088        assert!(ts.contains("url: string"));
1089        assert!(ts.contains("timeout?:")); // Optional field
1090        assert!(ts.contains("_trace?: TraceContext"));
1091    }
1092
1093    #[test]
1094    fn test_worker_brick_implements_brick() {
1095        let worker = WorkerBrick::new("test")
1096            .message(BrickWorkerMessage::new(
1097                "init",
1098                BrickWorkerMessageDirection::ToWorker,
1099            ))
1100            .transition("uninitialized", "init", "ready");
1101
1102        assert_eq!(worker.brick_name(), "WorkerBrick");
1103        assert!(worker.assertions().is_empty());
1104        assert_eq!(worker.budget().total_ms, 1000);
1105        assert!(worker.to_html().is_empty());
1106        assert!(worker.to_css().is_empty());
1107        assert!(worker.test_id().is_none());
1108    }
1109
1110    #[test]
1111    fn test_worker_brick_verify_invalid_from_state() {
1112        let mut worker = WorkerBrick::new("test");
1113        worker
1114            .transitions
1115            .push(WorkerTransition::new("nonexistent", "event", "ready"));
1116        // Don't add the state to states list
1117
1118        let result = worker.verify();
1119        assert!(!result.is_valid());
1120    }
1121
1122    #[test]
1123    fn test_worker_brick_verify_invalid_to_state() {
1124        let mut worker = WorkerBrick::new("test");
1125        worker.states.push("from_state".to_string());
1126        worker
1127            .transitions
1128            .push(WorkerTransition::new("from_state", "event", "nonexistent"));
1129        // Don't add "nonexistent" to states
1130
1131        let result = worker.verify();
1132        assert!(!result.is_valid());
1133    }
1134
1135    #[test]
1136    fn test_worker_brick_verify_message_no_transition() {
1137        let worker = WorkerBrick::new("test").message(BrickWorkerMessage::new(
1138            "orphan",
1139            BrickWorkerMessageDirection::ToWorker,
1140        ));
1141
1142        let result = worker.verify();
1143        assert!(!result.is_valid());
1144    }
1145
1146    #[test]
1147    fn test_to_pascal_case_space_separator() {
1148        assert_eq!(to_pascal_case("hello world"), "HelloWorld");
1149    }
1150
1151    #[test]
1152    fn test_to_snake_case_leading_uppercase() {
1153        assert_eq!(to_snake_case("URL"), "u_r_l");
1154        assert_eq!(to_snake_case("ABC"), "a_b_c");
1155    }
1156
1157    #[test]
1158    fn test_brick_worker_message_direction_eq() {
1159        assert_eq!(
1160            BrickWorkerMessageDirection::ToWorker,
1161            BrickWorkerMessageDirection::ToWorker
1162        );
1163        assert_ne!(
1164            BrickWorkerMessageDirection::ToWorker,
1165            BrickWorkerMessageDirection::FromWorker
1166        );
1167    }
1168
1169    #[test]
1170    fn test_field_type_shared_array_buffer_rust() {
1171        assert_eq!(
1172            FieldType::SharedArrayBuffer.to_rust(),
1173            "js_sys::SharedArrayBuffer"
1174        );
1175    }
1176
1177    #[test]
1178    fn test_message_field_clone() {
1179        let field = MessageField::new("test", FieldType::String);
1180        let cloned = field.clone();
1181        assert_eq!(field.name, cloned.name);
1182        assert_eq!(field.field_type, cloned.field_type);
1183    }
1184
1185    #[test]
1186    fn test_worker_transition_clone() {
1187        let transition = WorkerTransition::new("a", "b", "c").with_action("action");
1188        let cloned = transition.clone();
1189        assert_eq!(transition.from, cloned.from);
1190        assert_eq!(transition.action, cloned.action);
1191    }
1192
1193    #[test]
1194    fn test_worker_brick_clone() {
1195        let worker = WorkerBrick::new("test")
1196            .message(BrickWorkerMessage::new(
1197                "msg",
1198                BrickWorkerMessageDirection::ToWorker,
1199            ))
1200            .transition("a", "msg", "b");
1201        let cloned = worker.clone();
1202        assert_eq!(worker.name, cloned.name);
1203        assert_eq!(worker.messages.len(), cloned.messages.len());
1204    }
1205}