1use crate::binding::{
21 has_parallel_marker, parse_mentions, resolve_mention, strip_parallel_marker, text_to_bindings,
22 BindingSpec, Mention, MentionResolutionError, ResolvedMention,
23};
24use crate::dag::StableDag;
25use chrono::{DateTime, Utc};
26use petgraph::stable_graph::NodeIndex;
27use serde::{Deserialize, Serialize};
28use std::collections::HashMap;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
32pub enum Role {
33 User,
34 Assistant,
35 System,
36 Tool,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct ChatMessage {
42 pub id: String,
43 pub content: String,
44 pub role: Role,
45 pub timestamp: DateTime<Utc>,
46}
47
48pub struct ChatWorkflow {
53 pub dag: StableDag<ChatMessage>,
55 message_counter: u32,
57 id_to_index: HashMap<String, NodeIndex>,
59}
60
61impl ChatWorkflow {
62 pub fn new() -> Self {
64 Self {
65 dag: StableDag::new(),
66 message_counter: 0,
67 id_to_index: HashMap::new(),
68 }
69 }
70
71 pub fn message_count(&self) -> usize {
73 self.dag.node_count()
74 }
75
76 pub fn add_message(&mut self, content: &str, role: Role) -> NodeIndex {
86 self.message_counter += 1;
87 let id = format!("msg-{:03}", self.message_counter);
88
89 let message = ChatMessage {
90 id: id.clone(),
91 content: content.to_string(),
92 role,
93 timestamp: Utc::now(),
94 };
95
96 let idx = self.dag.add_node(message);
97 self.id_to_index.insert(id, idx);
98
99 if self.message_counter > 1 {
102 if let Some(prev_idx) = self.get_index_by_number(self.message_counter - 1) {
103 self.dag.add_edge(prev_idx, idx);
104 }
105 }
106
107 idx
108 }
109
110 pub fn get_message_by_id(&self, id: &str) -> Option<&ChatMessage> {
112 self.id_to_index
113 .get(id)
114 .and_then(|idx| self.dag.node_weight(*idx))
115 }
116
117 pub fn get_message_by_index(&self, idx: NodeIndex) -> Option<&ChatMessage> {
123 self.dag.node_weight(idx)
124 }
125
126 pub fn get_message_by_number(&self, n: u32) -> Option<&ChatMessage> {
129 let id = format!("msg-{:03}", n);
130 self.get_message_by_id(&id)
131 }
132
133 pub fn get_index_by_number(&self, n: u32) -> Option<NodeIndex> {
135 let id = format!("msg-{:03}", n);
136 self.id_to_index.get(&id).copied()
137 }
138
139 pub fn current_message_number(&self) -> u32 {
146 self.message_counter
147 }
148
149 pub fn last_message(&self) -> Option<&ChatMessage> {
151 if self.message_counter == 0 {
152 return None;
153 }
154 self.get_message_by_number(self.message_counter)
155 }
156
157 pub fn last_message_index(&self) -> Option<NodeIndex> {
159 if self.message_counter == 0 {
160 return None;
161 }
162 self.get_index_by_number(self.message_counter)
163 }
164
165 pub fn add_message_parallel(&mut self, content: &str, role: Role) -> NodeIndex {
174 self.message_counter += 1;
175 let id = format!("msg-{:03}", self.message_counter);
176
177 let clean_content = strip_parallel_marker(content);
179
180 let message = ChatMessage {
181 id: id.clone(),
182 content: clean_content.to_string(),
183 role,
184 timestamp: Utc::now(),
185 };
186
187 let idx = self.dag.add_node(message);
188 self.id_to_index.insert(id, idx);
189
190 idx
192 }
193
194 pub fn add_message_auto(&mut self, content: &str, role: Role) -> NodeIndex {
199 if has_parallel_marker(content) {
200 self.add_message_parallel(content, role)
201 } else {
202 self.add_message(content, role)
203 }
204 }
205
206 pub fn get_mentions(&self, idx: NodeIndex) -> Vec<Mention> {
210 match self.get_message_by_index(idx) {
211 Some(msg) => parse_mentions(&msg.content),
212 None => vec![],
213 }
214 }
215
216 pub fn resolve_mention(
218 &self,
219 mention: &Mention,
220 ) -> Result<ResolvedMention, MentionResolutionError> {
221 resolve_mention(mention, self.message_counter)
222 }
223
224 pub fn get_bindings_for_message(
229 &self,
230 idx: NodeIndex,
231 ) -> Result<BindingSpec, MentionResolutionError> {
232 match self.get_message_by_index(idx) {
233 Some(msg) => text_to_bindings(&msg.content, self.message_counter),
234 None => Ok(BindingSpec::default()),
235 }
236 }
237
238 pub fn is_parallel_message(&self, idx: NodeIndex) -> bool {
242 match self.get_message_by_index(idx) {
243 Some(msg) => has_parallel_marker(&msg.content),
244 None => false,
245 }
246 }
247
248 pub fn add_edges_from_mentions(
260 &mut self,
261 target_idx: NodeIndex,
262 ) -> Result<usize, MentionResolutionError> {
263 let content = match self.get_message_by_index(target_idx) {
265 Some(msg) => msg.content.clone(),
266 None => return Ok(0),
267 };
268
269 let mentions = parse_mentions(&content);
271 let mut edges_added = 0;
272
273 for mention in mentions {
274 let resolved = resolve_mention(&mention, self.message_counter)?;
276
277 match resolved {
278 ResolvedMention::Single(n) => {
279 if let Some(source_idx) = self.get_index_by_number(n) {
280 if source_idx != target_idx {
282 self.dag.add_edge(source_idx, target_idx);
283 edges_added += 1;
284 }
285 }
286 }
287 ResolvedMention::Multiple(indices) => {
288 for n in indices {
289 if let Some(source_idx) = self.get_index_by_number(n) {
290 if source_idx != target_idx {
292 self.dag.add_edge(source_idx, target_idx);
293 edges_added += 1;
294 }
295 }
296 }
297 }
298 ResolvedMention::Empty => {}
299 }
300 }
301
302 Ok(edges_added)
303 }
304
305 pub fn add_message_with_mentions(
313 &mut self,
314 content: &str,
315 role: Role,
316 ) -> Result<NodeIndex, MentionResolutionError> {
317 let idx = self.add_message_auto(content, role);
318 self.add_edges_from_mentions(idx)?;
319 Ok(idx)
320 }
321
322 pub fn get_dependencies(&self, idx: NodeIndex) -> Vec<NodeIndex> {
326 self.dag.incoming_neighbors(idx).collect()
327 }
328
329 pub fn get_dependents(&self, idx: NodeIndex) -> Vec<NodeIndex> {
333 self.dag.outgoing_neighbors(idx).collect()
334 }
335
336 pub fn all_messages(&self) -> Vec<(NodeIndex, &ChatMessage)> {
344 (1..=self.message_counter)
345 .filter_map(|n| {
346 let idx = self.get_index_by_number(n)?;
347 let msg = self.get_message_by_index(idx)?;
348 Some((idx, msg))
349 })
350 .collect()
351 }
352
353 pub fn total_messages(&self) -> u32 {
355 self.message_counter
356 }
357
358 pub fn to_yaml(&self) -> String {
382 let mut yaml = String::new();
383
384 yaml.push_str("# Auto-generated from Nika Chat session\n");
386 yaml.push_str(&format!(
387 "# Generated: {}\n",
388 Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
389 ));
390 yaml.push_str("# Use `nika run <file>` to execute\n\n");
391
392 yaml.push_str("schema: \"nika/workflow@0.12\"\n");
393 yaml.push_str("provider: claude\n\n");
394
395 let mut tasks: Vec<(&str, &str, Vec<String>)> = Vec::new();
398
399 for (idx, msg) in self.all_messages() {
400 match msg.role {
401 Role::User => {
402 let mut deps = Vec::new();
404 for dep_idx in self.get_dependencies(idx) {
405 if let Some(dep_msg) = self.get_message_by_index(dep_idx) {
406 if dep_msg.role == Role::User {
407 deps.push(dep_msg.id.clone());
408 }
409 }
410 }
411 tasks.push((&msg.id, &msg.content, deps));
412 }
413 Role::Assistant | Role::System | Role::Tool => {
414 }
416 }
417 }
418
419 if tasks.is_empty() {
421 yaml.push_str("# No user messages to convert to tasks\n");
422 yaml.push_str("tasks: []\n");
423 } else {
424 yaml.push_str("tasks:\n");
425 for (id, content, deps) in &tasks {
426 let escaped = escape_yaml_string(content);
427 yaml.push_str(&format!(" - id: \"{}\"\n", id));
428 if !deps.is_empty() {
429 let dep_list: Vec<String> = deps.iter().map(|d| format!("\"{}\"", d)).collect();
430 yaml.push_str(&format!(" depends_on: [{}]\n", dep_list.join(", ")));
431 }
432 yaml.push_str(&format!(" infer: \"{}\"\n\n", escaped));
433 }
434 }
435
436 yaml
437 }
438}
439
440fn escape_yaml_string(s: &str) -> String {
442 s.replace('\\', "\\\\")
443 .replace('"', "\\\"")
444 .replace('\n', "\\n")
445 .replace('\r', "\\r")
446 .replace('\t', "\\t")
447}
448
449impl Default for ChatWorkflow {
450 fn default() -> Self {
451 Self::new()
452 }
453}
454
455const _: () = {
459 const fn assert_send_sync<T: Send + Sync>() {}
460 assert_send_sync::<ChatWorkflow>();
461};
462
463#[cfg(test)]
464mod tests {
465 use super::*;
466
467 #[test]
472 fn test_chat_workflow_new_creates_empty_dag() {
473 let workflow = ChatWorkflow::new();
474
475 assert_eq!(workflow.message_count(), 0);
476 assert_eq!(workflow.dag.node_count(), 0);
477 }
478
479 #[test]
480 fn test_chat_workflow_default() {
481 let workflow = ChatWorkflow::default();
482 assert_eq!(workflow.message_count(), 0);
483 }
484
485 #[test]
486 fn test_chat_workflow_send_sync() {
487 fn assert_send<T: Send>() {}
488 fn assert_sync<T: Sync>() {}
489
490 assert_send::<ChatWorkflow>();
492 assert_sync::<ChatWorkflow>();
493 }
494
495 #[test]
500 fn test_all_role_variants() {
501 let roles = [Role::User, Role::Assistant, Role::System, Role::Tool];
502 assert_eq!(roles.len(), 4);
503 }
504
505 #[test]
506 fn test_role_equality() {
507 assert_eq!(Role::User, Role::User);
508 assert_ne!(Role::User, Role::Assistant);
509 }
510
511 #[test]
512 fn test_role_clone() {
513 let role = Role::Assistant;
514 let cloned = role;
515 assert_eq!(role, cloned);
516 }
517
518 #[test]
519 fn test_role_serialization() {
520 let role = Role::User;
521 let json = serde_json::to_string(&role).unwrap();
522 let restored: Role = serde_json::from_str(&json).unwrap();
523 assert_eq!(role, restored);
524 }
525
526 #[test]
531 fn test_add_message_creates_node() {
532 let mut workflow = ChatWorkflow::new();
533
534 let idx = workflow.add_message("Hello!", Role::User);
535
536 assert_eq!(workflow.message_count(), 1);
537 assert_eq!(idx.index(), 0); }
539
540 #[test]
541 fn test_add_message_generates_sequential_ids() {
542 let mut workflow = ChatWorkflow::new();
543
544 workflow.add_message("First", Role::User);
545 workflow.add_message("Second", Role::Assistant);
546 workflow.add_message("Third", Role::User);
547
548 let msg1 = workflow.get_message_by_id("msg-001");
549 let msg2 = workflow.get_message_by_id("msg-002");
550 let msg3 = workflow.get_message_by_id("msg-003");
551
552 assert!(msg1.is_some());
553 assert!(msg2.is_some());
554 assert!(msg3.is_some());
555
556 assert_eq!(msg1.unwrap().content, "First");
557 assert_eq!(msg2.unwrap().content, "Second");
558 assert_eq!(msg3.unwrap().content, "Third");
559 }
560
561 #[test]
562 fn test_add_message_assigns_correct_role() {
563 let mut workflow = ChatWorkflow::new();
564
565 workflow.add_message("User msg", Role::User);
566 workflow.add_message("Assistant msg", Role::Assistant);
567 workflow.add_message("System msg", Role::System);
568 workflow.add_message("Tool msg", Role::Tool);
569
570 assert_eq!(
571 workflow.get_message_by_id("msg-001").unwrap().role,
572 Role::User
573 );
574 assert_eq!(
575 workflow.get_message_by_id("msg-002").unwrap().role,
576 Role::Assistant
577 );
578 assert_eq!(
579 workflow.get_message_by_id("msg-003").unwrap().role,
580 Role::System
581 );
582 assert_eq!(
583 workflow.get_message_by_id("msg-004").unwrap().role,
584 Role::Tool
585 );
586 }
587
588 #[test]
589 fn test_get_message_by_id_nonexistent() {
590 let workflow = ChatWorkflow::new();
591 assert!(workflow.get_message_by_id("msg-999").is_none());
592 }
593
594 #[test]
599 fn test_get_message_by_index() {
600 let mut workflow = ChatWorkflow::new();
601
602 let idx = workflow.add_message("Test message", Role::User);
603 let msg = workflow.get_message_by_index(idx);
604
605 assert!(msg.is_some());
606 assert_eq!(msg.unwrap().content, "Test message");
607 }
608
609 #[test]
610 fn test_get_message_by_number() {
611 let mut workflow = ChatWorkflow::new();
612
613 workflow.add_message("First", Role::User);
614 workflow.add_message("Second", Role::Assistant);
615
616 let msg1 = workflow.get_message_by_number(1);
618 let msg2 = workflow.get_message_by_number(2);
619 let msg3 = workflow.get_message_by_number(3);
620
621 assert_eq!(msg1.unwrap().content, "First");
622 assert_eq!(msg2.unwrap().content, "Second");
623 assert!(msg3.is_none());
624 }
625
626 #[test]
627 fn test_get_index_by_number() {
628 let mut workflow = ChatWorkflow::new();
629
630 let idx1 = workflow.add_message("First", Role::User);
631 let idx2 = workflow.add_message("Second", Role::Assistant);
632
633 assert_eq!(workflow.get_index_by_number(1), Some(idx1));
634 assert_eq!(workflow.get_index_by_number(2), Some(idx2));
635 assert_eq!(workflow.get_index_by_number(3), None);
636 }
637
638 #[test]
639 fn test_get_message_by_invalid_index() {
640 let workflow = ChatWorkflow::new();
641 let invalid_idx = NodeIndex::new(999);
642 assert!(workflow.get_message_by_index(invalid_idx).is_none());
643 }
644
645 #[test]
650 fn test_auto_edge_sequential_messages() {
651 let mut workflow = ChatWorkflow::new();
652
653 let idx1 = workflow.add_message("First", Role::User);
654 let idx2 = workflow.add_message("Second", Role::Assistant);
655 let idx3 = workflow.add_message("Third", Role::User);
656
657 assert!(workflow.dag.has_edge(idx1, idx2), "Should have edge 1 → 2");
659 assert!(workflow.dag.has_edge(idx2, idx3), "Should have edge 2 → 3");
660
661 assert!(
663 !workflow.dag.has_edge(idx2, idx1),
664 "Should NOT have edge 2 → 1"
665 );
666 assert!(
667 !workflow.dag.has_edge(idx3, idx2),
668 "Should NOT have edge 3 → 2"
669 );
670 }
671
672 #[test]
673 fn test_first_message_has_no_incoming_edge() {
674 let mut workflow = ChatWorkflow::new();
675
676 let idx1 = workflow.add_message("First message", Role::User);
677
678 assert_eq!(
680 workflow.dag.edge_count(),
681 0,
682 "First message should have no edges"
683 );
684
685 let _idx2 = workflow.add_message("Second message", Role::Assistant);
687 assert_eq!(
688 workflow.dag.edge_count(),
689 1,
690 "Should have exactly 1 edge after 2 messages"
691 );
692
693 assert!(workflow.dag.node_weight(idx1).is_some());
695 }
696
697 #[test]
702 fn test_current_message_number_empty() {
703 let workflow = ChatWorkflow::new();
704 assert_eq!(workflow.current_message_number(), 0);
705 }
706
707 #[test]
708 fn test_current_message_number_increments() {
709 let mut workflow = ChatWorkflow::new();
710
711 workflow.add_message("First", Role::User);
712 assert_eq!(workflow.current_message_number(), 1);
713
714 workflow.add_message("Second", Role::Assistant);
715 assert_eq!(workflow.current_message_number(), 2);
716
717 workflow.add_message("Third", Role::User);
718 assert_eq!(workflow.current_message_number(), 3);
719 }
720
721 #[test]
722 fn test_last_message_empty() {
723 let workflow = ChatWorkflow::new();
724 assert!(workflow.last_message().is_none());
725 }
726
727 #[test]
728 fn test_last_message_returns_most_recent() {
729 let mut workflow = ChatWorkflow::new();
730
731 workflow.add_message("First", Role::User);
732 assert_eq!(workflow.last_message().unwrap().content, "First");
733
734 workflow.add_message("Second", Role::Assistant);
735 assert_eq!(workflow.last_message().unwrap().content, "Second");
736
737 workflow.add_message("Third", Role::User);
738 assert_eq!(workflow.last_message().unwrap().content, "Third");
739 }
740
741 #[test]
742 fn test_last_message_index() {
743 let mut workflow = ChatWorkflow::new();
744
745 assert!(workflow.last_message_index().is_none());
746
747 let idx1 = workflow.add_message("First", Role::User);
748 assert_eq!(workflow.last_message_index(), Some(idx1));
749
750 let idx2 = workflow.add_message("Second", Role::Assistant);
751 assert_eq!(workflow.last_message_index(), Some(idx2));
752 }
753
754 #[test]
759 fn test_concurrent_access_with_mutex() {
760 use parking_lot::Mutex;
761 use std::sync::Arc;
762 use std::thread;
763
764 let workflow = Arc::new(Mutex::new(ChatWorkflow::new()));
765 let mut handles = vec![];
766
767 for i in 0..4 {
769 let wf = Arc::clone(&workflow);
770 handles.push(thread::spawn(move || {
771 for j in 0..5 {
772 let mut guard = wf.lock();
773 let role = if (i + j) % 2 == 0 {
774 Role::User
775 } else {
776 Role::Assistant
777 };
778 guard.add_message(&format!("Thread {} msg {}", i, j), role);
779 }
780 }));
781 }
782
783 for h in handles {
785 h.join().unwrap();
786 }
787
788 let guard = workflow.lock();
790 assert_eq!(guard.message_count(), 20);
791 assert_eq!(guard.current_message_number(), 20);
792
793 assert_eq!(guard.dag.edge_count(), 19); }
797
798 #[test]
799 fn test_send_sync_bounds_with_arc() {
800 use std::sync::Arc;
801
802 fn assert_send_sync<T: Send + Sync>() {}
804 assert_send_sync::<Arc<parking_lot::Mutex<ChatWorkflow>>>();
805
806 let workflow = Arc::new(parking_lot::Mutex::new(ChatWorkflow::new()));
808 let _cloned = Arc::clone(&workflow);
809 }
810
811 #[test]
816 fn test_add_message_parallel_no_auto_edge() {
817 let mut workflow = ChatWorkflow::new();
818
819 workflow.add_message("First", Role::User);
820 workflow.add_message_parallel("// Independent task", Role::User);
821
822 assert_eq!(workflow.message_count(), 2);
824 assert_eq!(workflow.dag.edge_count(), 0);
825 }
826
827 #[test]
828 fn test_add_message_parallel_strips_marker() {
829 let mut workflow = ChatWorkflow::new();
830
831 let idx = workflow.add_message_parallel("// Independent task", Role::User);
832
833 let msg = workflow.get_message_by_index(idx).unwrap();
835 assert_eq!(msg.content, "Independent task");
836 }
837
838 #[test]
839 fn test_add_message_auto_detects_parallel() {
840 let mut workflow = ChatWorkflow::new();
841
842 workflow.add_message_auto("First", Role::User);
843 workflow.add_message_auto("Second", Role::Assistant);
844 workflow.add_message_auto("// Parallel", Role::User);
845
846 assert_eq!(workflow.message_count(), 3);
848 assert_eq!(workflow.dag.edge_count(), 1); }
850
851 #[test]
852 fn test_add_message_auto_sequential() {
853 let mut workflow = ChatWorkflow::new();
854
855 workflow.add_message_auto("First", Role::User);
856 workflow.add_message_auto("Second", Role::Assistant);
857
858 assert_eq!(workflow.message_count(), 2);
860 assert_eq!(workflow.dag.edge_count(), 1);
861 }
862
863 #[test]
864 fn test_get_mentions_parses_content() {
865 let mut workflow = ChatWorkflow::new();
866
867 workflow.add_message("First message", Role::User);
868 workflow.add_message("Second message", Role::Assistant);
869 let idx = workflow.add_message("Based on @1 and @2", Role::User);
870
871 let mentions = workflow.get_mentions(idx);
872 assert_eq!(mentions.len(), 2);
873 assert_eq!(mentions[0], Mention::Number(1));
874 assert_eq!(mentions[1], Mention::Number(2));
875 }
876
877 #[test]
878 fn test_get_mentions_empty_for_no_mentions() {
879 let mut workflow = ChatWorkflow::new();
880
881 let idx = workflow.add_message("No mentions here", Role::User);
882
883 let mentions = workflow.get_mentions(idx);
884 assert!(mentions.is_empty());
885 }
886
887 #[test]
888 fn test_resolve_mention_uses_message_count() {
889 let mut workflow = ChatWorkflow::new();
890
891 workflow.add_message("First", Role::User);
892 workflow.add_message("Second", Role::Assistant);
893 workflow.add_message("Third", Role::User);
894
895 let result = workflow.resolve_mention(&Mention::Last);
897 assert_eq!(result, Ok(ResolvedMention::Single(3)));
898
899 let result = workflow.resolve_mention(&Mention::All);
901 assert_eq!(result, Ok(ResolvedMention::Multiple(vec![1, 2, 3])));
902 }
903
904 #[test]
905 fn test_get_bindings_for_message() {
906 let mut workflow = ChatWorkflow::new();
907
908 workflow.add_message("First", Role::User);
909 workflow.add_message("Second", Role::Assistant);
910 let idx = workflow.add_message("Based on @1", Role::User);
911
912 let spec = workflow.get_bindings_for_message(idx).unwrap();
913 assert_eq!(spec.len(), 1);
914 assert!(spec.contains_key("ref_1"));
915 assert_eq!(spec["ref_1"].path, "msg-001.output");
916 }
917
918 #[test]
919 fn test_get_bindings_for_message_with_last() {
920 let mut workflow = ChatWorkflow::new();
921
922 workflow.add_message("First", Role::User);
923 workflow.add_message("Second", Role::Assistant);
924 let idx = workflow.add_message("Continue from @last", Role::User);
925
926 let spec = workflow.get_bindings_for_message(idx).unwrap();
928 assert_eq!(spec.len(), 1);
929 assert!(spec.contains_key("ref_3")); }
931
932 #[test]
933 fn test_is_parallel_message() {
934 let mut workflow = ChatWorkflow::new();
935
936 let idx1 = workflow.add_message("Normal", Role::User);
937 let idx2 = workflow.add_message("// Parallel", Role::User);
938
939 assert!(!workflow.is_parallel_message(idx1));
940 assert!(workflow.is_parallel_message(idx2));
941 }
942
943 #[test]
948 fn test_add_edges_from_mentions_single() {
949 let mut workflow = ChatWorkflow::new();
950
951 workflow.add_message("First", Role::User);
952 workflow.add_message("Second", Role::Assistant);
953 let idx3 = workflow.add_message("Based on @1", Role::User);
954
955 let edges_added = workflow.add_edges_from_mentions(idx3).unwrap();
956 assert_eq!(edges_added, 1);
957
958 let deps = workflow.get_dependencies(idx3);
960 assert_eq!(deps.len(), 2); }
962
963 #[test]
964 fn test_add_edges_from_mentions_multiple() {
965 let mut workflow = ChatWorkflow::new();
966
967 workflow.add_message("First", Role::User);
968 workflow.add_message("Second", Role::Assistant);
969 let idx3 = workflow.add_message("Based on @1 and @2", Role::User);
970
971 let edges_added = workflow.add_edges_from_mentions(idx3).unwrap();
972 assert_eq!(edges_added, 2);
973 }
974
975 #[test]
976 fn test_add_edges_from_mentions_range() {
977 let mut workflow = ChatWorkflow::new();
978
979 workflow.add_message("First", Role::User);
980 workflow.add_message("Second", Role::Assistant);
981 workflow.add_message("Third", Role::User);
982 let idx4 = workflow.add_message("Summarize @1..3", Role::User);
983
984 let edges_added = workflow.add_edges_from_mentions(idx4).unwrap();
985 assert_eq!(edges_added, 3); }
987
988 #[test]
989 fn test_add_edges_from_mentions_no_self_loop() {
990 let mut workflow = ChatWorkflow::new();
991
992 workflow.add_message("First", Role::User);
993 workflow.add_message("Second", Role::Assistant);
994 let idx3 = workflow.add_message("Self reference @3", Role::User);
996
997 let edges_added = workflow.add_edges_from_mentions(idx3).unwrap();
1002 assert_eq!(edges_added, 0); }
1004
1005 #[test]
1006 fn test_add_edges_from_mentions_no_mentions() {
1007 let mut workflow = ChatWorkflow::new();
1008
1009 workflow.add_message("First", Role::User);
1010 let idx2 = workflow.add_message("No mentions here", Role::User);
1011
1012 let edges_added = workflow.add_edges_from_mentions(idx2).unwrap();
1013 assert_eq!(edges_added, 0);
1014 }
1015
1016 #[test]
1017 fn test_add_message_with_mentions() {
1018 let mut workflow = ChatWorkflow::new();
1019
1020 workflow.add_message("First", Role::User);
1021 workflow.add_message("Second", Role::Assistant);
1022 let idx3 = workflow
1023 .add_message_with_mentions("Based on @1", Role::User)
1024 .unwrap();
1025
1026 let deps = workflow.get_dependencies(idx3);
1028 assert_eq!(deps.len(), 2);
1029 }
1030
1031 #[test]
1032 fn test_add_message_with_mentions_parallel() {
1033 let mut workflow = ChatWorkflow::new();
1034
1035 workflow.add_message("First", Role::User);
1036 workflow.add_message("Second", Role::Assistant);
1037 let idx3 = workflow
1038 .add_message_with_mentions("// Based on @1", Role::User)
1039 .unwrap();
1040
1041 let deps = workflow.get_dependencies(idx3);
1043 assert_eq!(deps.len(), 1);
1044 }
1045
1046 #[test]
1047 fn test_add_message_with_mentions_error() {
1048 let mut workflow = ChatWorkflow::new();
1049
1050 workflow.add_message("First", Role::User);
1051
1052 let result = workflow.add_message_with_mentions("Reference @10", Role::User);
1054 assert!(result.is_err());
1055 }
1056
1057 #[test]
1058 fn test_get_dependencies() {
1059 let mut workflow = ChatWorkflow::new();
1060
1061 let idx1 = workflow.add_message("First", Role::User);
1062 let idx2 = workflow.add_message("Second", Role::Assistant);
1063 let idx3 = workflow.add_message("Third", Role::User);
1064
1065 let deps = workflow.get_dependencies(idx3);
1067 assert_eq!(deps.len(), 1);
1068 assert_eq!(deps[0], idx2);
1069
1070 let deps = workflow.get_dependencies(idx1);
1072 assert!(deps.is_empty());
1073 }
1074
1075 #[test]
1076 fn test_get_dependents() {
1077 let mut workflow = ChatWorkflow::new();
1078
1079 let idx1 = workflow.add_message("First", Role::User);
1080 let idx2 = workflow.add_message("Second", Role::Assistant);
1081 workflow.add_message("Third", Role::User);
1082
1083 let dependents = workflow.get_dependents(idx1);
1085 assert_eq!(dependents.len(), 1);
1086 assert_eq!(dependents[0], idx2);
1087 }
1088
1089 #[test]
1090 fn test_complex_mention_dag() {
1091 let mut workflow = ChatWorkflow::new();
1092
1093 workflow
1101 .add_message_with_mentions("What is Rust?", Role::User)
1102 .unwrap();
1103 workflow
1104 .add_message_with_mentions("Rust is a systems programming language...", Role::Assistant)
1105 .unwrap();
1106 workflow
1107 .add_message_with_mentions("// What is Go?", Role::User)
1108 .unwrap();
1109 workflow
1110 .add_message_with_mentions("Go is a programming language...", Role::Assistant)
1111 .unwrap();
1112 let idx5 = workflow
1113 .add_message_with_mentions("Compare @2 and @4", Role::User)
1114 .unwrap();
1115
1116 let deps = workflow.get_dependencies(idx5);
1118 assert_eq!(deps.len(), 3); assert!(workflow.dag.edge_count() >= 4);
1124 }
1125
1126 #[test]
1131 fn test_to_yaml_empty_workflow() {
1132 let workflow = ChatWorkflow::new();
1133 let yaml = workflow.to_yaml();
1134
1135 assert!(yaml.contains("# Auto-generated from Nika Chat session"));
1137 assert!(yaml.contains("schema: \"nika/workflow@0.12\""));
1138 assert!(yaml.contains("provider: claude"));
1139
1140 assert!(yaml.contains("tasks: []"));
1142 assert!(!yaml.contains("flows:"));
1144 }
1145
1146 #[test]
1147 fn test_to_yaml_single_user_message() {
1148 let mut workflow = ChatWorkflow::new();
1149 workflow.add_message("What is Rust?", Role::User);
1150
1151 let yaml = workflow.to_yaml();
1152
1153 assert!(yaml.contains("tasks:"));
1155 assert!(yaml.contains("- id: \"msg-001\""));
1156 assert!(yaml.contains("infer: \"What is Rust?\""));
1157 }
1158
1159 #[test]
1160 fn test_to_yaml_skips_non_user_messages() {
1161 let mut workflow = ChatWorkflow::new();
1162 workflow.add_message("What is Rust?", Role::User); workflow.add_message("Rust is a systems language", Role::Assistant); workflow.add_message("System initialized", Role::System); workflow.add_message("Tool output", Role::Tool); workflow.add_message("Tell me more", Role::User); let yaml = workflow.to_yaml();
1169
1170 assert!(yaml.contains("- id: \"msg-001\""));
1172 assert!(yaml.contains("- id: \"msg-005\""));
1173
1174 assert!(!yaml.contains("- id: \"msg-002\""));
1176 assert!(!yaml.contains("- id: \"msg-003\""));
1177 assert!(!yaml.contains("- id: \"msg-004\""));
1178 }
1179
1180 #[test]
1181 fn test_to_yaml_escapes_quotes() {
1182 let mut workflow = ChatWorkflow::new();
1183 workflow.add_message("Generate \"hello world\" program", Role::User);
1184
1185 let yaml = workflow.to_yaml();
1186
1187 assert!(yaml.contains(r#"infer: "Generate \"hello world\" program""#));
1189 }
1190
1191 #[test]
1192 fn test_to_yaml_escapes_multiline() {
1193 let mut workflow = ChatWorkflow::new();
1194 workflow.add_message("Line 1\nLine 2", Role::User);
1195
1196 let yaml = workflow.to_yaml();
1197
1198 assert!(yaml.contains("Line 1\\nLine 2"));
1200 }
1201
1202 #[test]
1203 fn test_to_yaml_with_mention_creates_depends_on() {
1204 let mut workflow = ChatWorkflow::new();
1205 workflow
1206 .add_message_with_mentions("What is Rust?", Role::User)
1207 .unwrap(); workflow
1209 .add_message_with_mentions("Rust is...", Role::Assistant)
1210 .unwrap(); workflow
1212 .add_message_with_mentions("Expand on @1", Role::User)
1213 .unwrap(); let yaml = workflow.to_yaml();
1216
1217 assert!(yaml.contains("depends_on:"));
1219 assert!(yaml.contains("\"msg-001\""));
1220 assert!(!yaml.contains("flows:"));
1222 }
1223
1224 #[test]
1225 fn test_to_yaml_multiple_mentions_create_depends_on() {
1226 let mut workflow = ChatWorkflow::new();
1227 workflow
1228 .add_message_with_mentions("First question", Role::User)
1229 .unwrap(); workflow
1231 .add_message_with_mentions("Answer 1", Role::Assistant)
1232 .unwrap(); workflow
1234 .add_message_with_mentions("Second question", Role::User)
1235 .unwrap(); workflow
1237 .add_message_with_mentions("Answer 2", Role::Assistant)
1238 .unwrap(); workflow
1240 .add_message_with_mentions("Compare @1 and @3", Role::User)
1241 .unwrap(); let yaml = workflow.to_yaml();
1244
1245 assert!(yaml.contains("depends_on:"));
1247 assert!(yaml.contains("\"msg-001\""));
1248 assert!(yaml.contains("\"msg-003\""));
1249 assert!(!yaml.contains("flows:"));
1251 }
1252
1253 #[test]
1254 fn test_escape_yaml_string_simple() {
1255 assert_eq!(escape_yaml_string("hello"), "hello");
1256 }
1257
1258 #[test]
1259 fn test_escape_yaml_string_with_quotes() {
1260 assert_eq!(escape_yaml_string(r#"say "hi""#), r#"say \"hi\""#);
1261 }
1262
1263 #[test]
1264 fn test_escape_yaml_string_with_newline() {
1265 assert_eq!(escape_yaml_string("line1\nline2"), "line1\\nline2");
1266 }
1267
1268 #[test]
1269 fn test_escape_yaml_string_with_backslash() {
1270 assert_eq!(escape_yaml_string(r"path\to\file"), r"path\\to\\file");
1271 }
1272
1273 #[test]
1274 fn test_escape_yaml_string_complex() {
1275 let input = "He said \"hello\"\nThen walked away";
1276 let expected = r#"He said \"hello\"\nThen walked away"#;
1277 assert_eq!(escape_yaml_string(input), expected);
1278 }
1279
1280 #[test]
1281 fn test_to_yaml_round_trip_parseable() {
1282 let mut workflow = ChatWorkflow::new();
1284 workflow.add_message("Question 1", Role::User);
1285 workflow.add_message("Answer 1", Role::Assistant);
1286 workflow.add_message("Question 2", Role::User);
1287
1288 let yaml = workflow.to_yaml();
1289
1290 let parsed: serde_json::Value = crate::serde_yaml::from_str(&yaml).expect("Valid YAML");
1293
1294 assert!(parsed["schema"].as_str().is_some());
1296 assert!(parsed["tasks"].is_array()); assert!(parsed.get("flows").is_none());
1299 }
1300}