1use super::{Brick, BrickAssertion, BrickBudget, BrickVerification};
27use std::time::Duration;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum BrickWorkerMessageDirection {
32 ToWorker,
34 FromWorker,
36 Bidirectional,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum FieldType {
43 String,
45 Number,
47 Boolean,
49 SharedArrayBuffer,
51 Float32Array,
53 Object(Vec<MessageField>),
55 Optional(Box<FieldType>),
57}
58
59impl FieldType {
60 #[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 #[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#[derive(Debug, Clone, PartialEq, Eq)]
97pub struct MessageField {
98 pub name: String,
100 pub field_type: FieldType,
102 pub required: bool,
104}
105
106impl MessageField {
107 #[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 #[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#[derive(Debug, Clone)]
130pub struct BrickWorkerMessage {
131 pub name: String,
133 pub direction: BrickWorkerMessageDirection,
135 pub fields: Vec<MessageField>,
137 pub trace_context: bool,
139}
140
141impl BrickWorkerMessage {
142 #[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, }
151 }
152
153 #[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 #[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 #[must_use]
169 pub fn without_trace(mut self) -> Self {
170 self.trace_context = false;
171 self
172 }
173
174 #[must_use]
176 pub fn js_type_name(&self) -> String {
177 self.name.to_lowercase()
178 }
179
180 #[must_use]
182 pub fn rust_type_name(&self) -> String {
183 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#[derive(Debug, Clone)]
202pub struct WorkerTransition {
203 pub from: String,
205 pub message: String,
207 pub to: String,
209 pub action: Option<String>,
211}
212
213impl WorkerTransition {
214 #[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 #[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#[derive(Debug, Clone)]
235pub struct WorkerBrick {
236 name: String,
238 messages: Vec<BrickWorkerMessage>,
240 transitions: Vec<WorkerTransition>,
242 initial_state: String,
244 states: Vec<String>,
246}
247
248impl WorkerBrick {
249 #[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 #[must_use]
263 pub fn message(mut self, msg: BrickWorkerMessage) -> Self {
264 self.messages.push(msg);
265 self
266 }
267
268 #[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 #[must_use]
280 pub fn initial(mut self, state: impl Into<String>) -> Self {
281 self.initial_state = state.into();
282 self
283 }
284
285 #[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 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 #[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 #[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 #[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 #[must_use]
365 pub fn to_worker_js(&self) -> String {
366 let mut js = String::new();
367
368 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 js.push_str(&format!("let workerState = '{}';\n\n", self.initial_state));
377
378 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 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 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 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 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 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 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 js.push_str(&format!(
445 "console.log('[Worker] {} module loaded');\n",
446 to_pascal_case(&self.name)
447 ));
448
449 js
450 }
451
452 #[must_use]
454 pub fn to_rust_bindings(&self) -> String {
455 let mut rust = String::new();
456
457 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 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 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 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 #[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 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 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 &[]
572 }
573
574 fn budget(&self) -> BrickBudget {
575 BrickBudget::uniform(1000)
577 }
578
579 fn verify(&self) -> BrickVerification {
580 let mut passed = Vec::new();
581 let mut failed = Vec::new();
582
583 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 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 String::new()
645 }
646
647 fn to_css(&self) -> String {
648 String::new()
650 }
651
652 fn test_id(&self) -> Option<&str> {
653 None
654 }
655}
656
657fn 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
676fn 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 #[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); }
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"); 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); }
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); }
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?:")); 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 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 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}